/* ═══════════════════════════════════════════════════════
   Kling Motion 2.6 — Frontend (Weavy + KIE.AI)
   ═══════════════════════════════════════════════════════ */

const $ = (id) => document.getElementById(id);

// ─────────────────────────────────────────────────────
// CLIENT-SIDE ERROR SANITIZER (defense in depth)
// ─────────────────────────────────────────────────────
// Backend sudah menyanitasi error, tapi sebagai pengaman tambahan,
// filter pattern teknis yang TIDAK boleh ditampilkan ke user.
const _SENSITIVE_PATTERNS = [
  /kwaivgi[\/\\]+rkling-?v?\d?-?[\w-]*/gi,
  /kling-?v?\d?-?[\w-]*motion-?control/gi,
  /https?:\/\/(api\.)?weavy\.ai[^\s]*/gi,
  /https?:\/\/app\.weavy\.ai[^\s]*/gi,
  /https?:\/\/replicate\.com[^\s]*/gi,
  /https?:\/\/replicate\.delivery[^\s]*/gi,
  /https?:\/\/[\w.-]*fal\.ai[^\s]*/gi,
  /https?:\/\/[\w.-]*figma\.com[^\s]*/gi,
  /\bweavy\b/gi,
  /\breplicate\b/gi,
  /\bfal\.ai\b/gi,
  /\bfigma\b/gi,
  /\bfirebase\b/gi,
  /Insufficient\s*credits?\s*amount/gi,
  /Failed\s*running\s*[\w\/\\-]+/gi,
  /\bError:\s+[A-Z][\w]*Error/g,
  /\b\d{3}\s+Unauthorized\b/gi,
  /\b\d{3}\s+Forbidden\b/gi,
  /\b\d{3}\s+Bad\s*Request\b/gi,
  /\b\d{3}\s+Internal\s*Server\s*Error\b/gi,
  /\b\d{3}\s+Service\s*Unavailable\b/gi,
  /\binternalErrorCode[^\s)]*/gi,
  /\(code\s*\d+\)/gi,
];

function sanitizeErrorForUser(rawError) {
  if (!rawError) return "Generate gagal. Silakan coba lagi dalam beberapa saat atau hubungi Admin.";
  let s = String(rawError);

  // If any sensitive pattern found, replace with safe generic message
  for (const pat of _SENSITIVE_PATTERNS) {
    if (pat.test(s)) {
      return "Generate gagal. Silakan coba lagi dalam beberapa saat atau hubungi Admin.";
    }
  }
  // Cap length
  if (s.length > 300) s = s.substring(0, 297) + "...";
  return s;
}

const els = {
  poolBanner: $("pool-banner"),
  poolInfo: $("pool-info"),
  poolInfoFooter: $("pool-info-footer"),

  imageDz: $("image-dropzone"),
  imageFile: $("image-file"),
  imagePreview: $("image-preview"),
  imagePreviewBox: $("image-preview-box"),

  videoDz: $("video-dropzone"),
  videoFile: $("video-file"),
  videoPreview: $("video-preview"),
  videoPreviewBox: $("video-preview-box"),

  prompt: $("prompt"),
  modelType: $("model_type"),
  modelVersion: $("model_version"),
  orientation: $("character_orientation"),
  keepSound: $("keep_original_sound"),

  generateBtn: $("generate-btn"),
  progressContainer: $("progress-container"),
  status: $("status"),
  result: $("result"),
  resultRetention: $("result-retention"),
  resultGallery: $("result-gallery"),

  // Voucher elements
  loginModal: $("login-modal"),
  voucherInput: $("voucher-input"),
  submitVoucherBtn: $("submit-voucher-btn"),
  modalStatus: $("modal-status"),
  interactiveContainer: $("interactive-container"),
  voucherInfoCard: $("voucher-info-card"),
  userVoucherCode: $("user-voucher-code"),
  userVoucherTier: $("user-voucher-tier"),
  userVoucherQuota: $("user-voucher-quota"),
  userVoucherRemaining: $("user-voucher-remaining"),
  logoutVoucherBtn: $("logout-voucher-btn"),

  // Expiring popup
  expiringPopup: $("expiring-popup"),
  expiringSnoozeBtn: $("expiring-snooze-btn"),
  expiringCloseBtn: $("expiring-close-btn"),
};

const VOUCHER_KEY = "vip_voucher_code";
const TIER_KEY = "vip_voucher_tier";
const UPLOAD_IMG_KEY = "upload_ref_image";
const UPLOAD_VID_KEY = "upload_ref_video";
const RESULT_KEY = "last_generated_results";
const LEGACY_RESULT_KEY = "last_generated_result";
const ACTIVE_GEN_KEY = "active_generation";
const RESULT_RETENTION_MS = 24 * 60 * 60 * 1000;
const RESULT_HISTORY_LIMIT = 10;
const EXPIRING_SNOOZE_KEY = "expiring_snooze_until";

const state = {
  image: { url: "", meta: {}, uploading: false },
  video: { url: "", meta: {}, uploading: false },
  recipeId: "",
  voucher: localStorage.getItem(VOUCHER_KEY) || "",
  tier: localStorage.getItem(TIER_KEY) || "",
};
let resultExpiryInterval = null;
let maintenanceInterval = null;

const activePolls = new Map();
const justFinishedIds = new Set();

// ═══════════════════════════════════════════════════════
// PERSISTENCE: Restore uploads & results from localStorage
// ═══════════════════════════════════════════════════════

function saveUploadToStorage(kind) {
  const key = kind === "image" ? UPLOAD_IMG_KEY : UPLOAD_VID_KEY;
  localStorage.setItem(key, JSON.stringify({
    url: state[kind].url,
    meta: state[kind].meta,
    savedAt: Date.now(),
  }));
}

function removeUploadFromStorage(kind) {
  const key = kind === "image" ? UPLOAD_IMG_KEY : UPLOAD_VID_KEY;
  localStorage.removeItem(key);
}

function clearAllUploadStorage() {
  localStorage.removeItem(UPLOAD_IMG_KEY);
  localStorage.removeItem(UPLOAD_VID_KEY);
}

function makeStoredResult(url, status = "completed", extra = {}) {
  const savedAt = Date.now();
  return {
    id: `result_${savedAt}_${Math.random().toString(36).slice(2, 8)}`,
    url: url || "",
    savedAt,
    expiresAt: savedAt + RESULT_RETENTION_MS,
    status,
    ...extra
  };
}

function normalizeStoredResultEntry(entry) {
  if (!entry) return null;
  const savedAt = Number(entry.savedAt || Date.now());
  const expiresAt = Number(entry.expiresAt || (savedAt + RESULT_RETENTION_MS));
  if (!Number.isFinite(savedAt) || !Number.isFinite(expiresAt)) return null;
  if (expiresAt <= Date.now()) return null;

  const id = entry.id || `result_${savedAt}_${Math.random().toString(36).slice(2, 8)}`;
  const status = entry.status || "completed";

  if (status === "processing") {
    return {
      id,
      savedAt,
      expiresAt,
      status,
      progress: typeof entry.progress === "number" ? entry.progress : 0,
      statusText: entry.statusText || "Mengantre...",
      accountId: entry.accountId,
      batchId: entry.batchId,
      recipeId: entry.recipeId,
      runId: entry.runId,
      payload: entry.payload,
    };
  } else if (status === "failed") {
    return {
      id,
      savedAt,
      expiresAt,
      status,
      error: sanitizeErrorForUser(entry.error || "Gagal"),
      payload: entry.payload,
    };
  } else {
    if (typeof entry.url !== "string" || !entry.url) return null;
    return {
      id,
      url: entry.url,
      savedAt,
      expiresAt,
      status: "completed",
    };
  }
}

function readStoredResults() {
  const raw = localStorage.getItem(RESULT_KEY);
  const legacyRaw = localStorage.getItem(LEGACY_RESULT_KEY);
  let parsed = [];

  try {
    if (raw) {
      const data = JSON.parse(raw);
      parsed = Array.isArray(data) ? data : [];
    } else if (legacyRaw) {
      const legacy = JSON.parse(legacyRaw);
      parsed = legacy ? [legacy] : [];
    }
  } catch {
    parsed = [];
  }

  const normalized = parsed
    .map(normalizeStoredResultEntry)
    .filter(Boolean)
    .sort((a, b) => b.savedAt - a.savedAt)
    .slice(0, RESULT_HISTORY_LIMIT);

  localStorage.setItem(RESULT_KEY, JSON.stringify(normalized));
  localStorage.removeItem(LEGACY_RESULT_KEY);
  return normalized;
}

function writeStoredResults(results) {
  const normalized = results
    .map(normalizeStoredResultEntry)
    .filter(Boolean)
    .sort((a, b) => b.savedAt - a.savedAt)
    .slice(0, RESULT_HISTORY_LIMIT);
  localStorage.setItem(RESULT_KEY, JSON.stringify(normalized));
  localStorage.removeItem(LEGACY_RESULT_KEY);
  return normalized;
}

function addProcessingResultToStorage(entryId, payload) {
  const current = readStoredResults();
  const newEntry = {
    id: entryId,
    savedAt: Date.now(),
    expiresAt: Date.now() + RESULT_RETENTION_MS,
    status: "processing",
    progress: 5,
    statusText: "Menghubungkan ke API server...",
    payload,
  };
  current.unshift(newEntry);
  writeStoredResults(current);
  return newEntry;
}

function updateStoredResultEntry(id, updates) {
  const current = readStoredResults();
  const item = current.find(r => r.id === id);
  if (item) {
    Object.assign(item, updates);
    writeStoredResults(current);
  }
}

function stopResultExpiryTimer() {
  if (resultExpiryInterval) {
    clearInterval(resultExpiryInterval);
    resultExpiryInterval = null;
  }
}

function formatRetentionRemaining(diffMs) {
  const totalSeconds = Math.max(0, Math.floor(diffMs / 1000));
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;
  return `${String(hours).padStart(2, "0")}j ${String(minutes).padStart(2, "0")}m ${String(seconds).padStart(2, "0")}d`;
}

function formatSavedAt(ts) {
  try {
    return new Date(ts).toLocaleString("id-ID", {
      day: "2-digit",
      month: "short",
      hour: "2-digit",
      minute: "2-digit",
    });
  } catch {
    return "-";
  }
}

function updateResultRetentionLabels() {
  const results = readStoredResults();
  if (!results.length) {
    clearStoredResult();
    return;
  }

  if (els.resultRetention) {
    els.resultRetention.hidden = false;
    const activeResults = results.filter(r => r.status === "completed" || r.status === "processing");
    const ttlH = Math.round((state.assetTtlMs || RESULT_RETENTION_MS) / 3600000);
    const storageLoc = `tersimpan ${ttlH} jam di server (proxy streaming)`;
    els.resultRetention.textContent = `Riwayat preview ${storageLoc} · maksimal ${RESULT_HISTORY_LIMIT} hasil · ${activeResults.length} preview aktif`;
  }

  document.querySelectorAll("[data-result-expires]").forEach((node) => {
    const expiresAt = Number(node.getAttribute("data-result-expires"));
    const remaining = expiresAt - Date.now();
    if (remaining <= 0) {
      renderStoredResults();
      return;
    }
    node.textContent = `Hilang dalam ${formatRetentionRemaining(remaining)}`;
  });
}

function startResultExpiryTimer() {
  stopResultExpiryTimer();
  updateResultRetentionLabels();
  resultExpiryInterval = setInterval(updateResultRetentionLabels, 1000);
}

function renderStoredResults() {
  const results = readStoredResults();
  if (!els.resultGallery) return;

  if (!results.length) {
    clearStoredResult();
    return;
  }

  els.resultGallery.innerHTML = "";
  const fragment = document.createDocumentFragment();

  results.forEach((item, index) => {
    const card = document.createElement("article");
    card.className = "result-card";

    const head = document.createElement("div");
    head.className = "result-card-head";

    const titleWrap = document.createElement("div");
    titleWrap.className = "result-card-title-wrap";

    const title = document.createElement("strong");
    title.className = "result-card-title";
    title.textContent = `Hasil #${results.length - index}`;

    const meta = document.createElement("span");
    meta.className = "result-card-meta";
    meta.textContent = `Disimpan ${formatSavedAt(item.savedAt)}`;

    const retention = document.createElement("span");
    retention.className = "result-card-retention";
    retention.setAttribute("data-result-expires", String(item.expiresAt));

    titleWrap.append(title, meta);
    head.append(titleWrap, retention);
    card.appendChild(head);

    if (item.status === "processing") {
      // Progress Bar
      const progressContainer = document.createElement("div");
      progressContainer.className = "result-card-progress-container";

      const progressBar = document.createElement("div");
      progressBar.className = "result-card-progress-bar";

      const progressFill = document.createElement("div");
      progressFill.className = "result-card-progress-fill";
      progressFill.style.width = `${item.progress}%`;

      const progressStatus = document.createElement("div");
      progressStatus.className = "result-card-progress-status pulse-text";
      progressStatus.textContent = `${item.progress}% - ${item.statusText}`;

      progressBar.appendChild(progressFill);
      progressContainer.append(progressBar, progressStatus);
      card.appendChild(progressContainer);

      // Cancel button
      const actions = document.createElement("div");
      actions.className = "result-card-actions";

      const removeBtn = document.createElement("button");
      removeBtn.type = "button";
      removeBtn.className = "ghost result-remove-btn";
      removeBtn.setAttribute("data-remove-result", item.id);
      removeBtn.textContent = "Batal";

      actions.appendChild(removeBtn);
      card.appendChild(actions);
    } else if (item.status === "failed") {
      // Error message box
      const errorContainer = document.createElement("div");
      errorContainer.className = "result-card-error-container";

      const errorBox = document.createElement("div");
      errorBox.className = "result-card-error-box";

      const errorTitle = document.createElement("div");
      errorTitle.className = "result-card-error-title";
      errorTitle.innerHTML = `<span>⚠️</span> Gagal`;

      const errorText = document.createElement("div");
      errorText.textContent = sanitizeErrorForUser(item.error);

      errorBox.append(errorTitle, errorText);
      errorContainer.appendChild(errorBox);
      card.appendChild(errorContainer);

      // Actions (Retry and Hapus buttons)
      const actions = document.createElement("div");
      actions.className = "result-card-actions";

      if (item.payload) {
        const retryBtn = document.createElement("button");
        retryBtn.type = "button";
        retryBtn.className = "primary result-retry-btn";
        retryBtn.style.width = "auto";
        retryBtn.style.padding = "0.5rem 0.85rem";
        retryBtn.style.fontSize = "0.85rem";
        retryBtn.setAttribute("data-retry-result", item.id);
        retryBtn.textContent = "🔄 Coba Lagi";
        actions.appendChild(retryBtn);
      }

      const removeBtn = document.createElement("button");
      removeBtn.type = "button";
      removeBtn.className = "ghost result-remove-btn";
      removeBtn.setAttribute("data-remove-result", item.id);
      removeBtn.textContent = "Hapus";

      actions.appendChild(removeBtn);
      card.appendChild(actions);
    } else {
      // Completed - show preview placeholder first, load video only on click
      const previewWrapper = document.createElement("div");
      previewWrapper.className = "result-card-video"; // Keeps the aspect-ratio and basic sizing
      previewWrapper.style.position = "relative";
      previewWrapper.style.cursor = "pointer";
      previewWrapper.style.display = "flex";
      previewWrapper.style.flexDirection = "column";
      previewWrapper.style.justifyContent = "center";
      previewWrapper.style.alignItems = "center";
      previewWrapper.style.background = "linear-gradient(135deg, #0f1015 0%, #050508 100%)";
      previewWrapper.style.border = "1px solid rgba(255, 102, 0, 0.15)";
      previewWrapper.style.transition = "border-color 0.25s, box-shadow 0.25s";

      // Glow overlay and play button text
      const playBtn = document.createElement("div");
      playBtn.style.display = "flex";
      playBtn.style.flexDirection = "column";
      playBtn.style.alignItems = "center";
      playBtn.style.gap = "8px";
      playBtn.style.pointerEvents = "none"; // Let clicks pass to wrapper

      playBtn.innerHTML = `
        <div class="play-icon-glow" style="width: 50px; height: 50px; border-radius: 50%; background: rgba(255, 102, 0, 0.1); border: 2px solid #ff6600; display: flex; justify-content: center; align-items: center; box-shadow: 0 0 15px rgba(255, 102, 0, 0.3); transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), background 0.2s, box-shadow 0.2s;">
          <span style="font-size: 1.3rem; margin-left: 3px; color: #ff6600;">▶</span>
        </div>
        <span style="font-size: 0.8rem; font-weight: 600; color: rgba(255,255,255,0.75); letter-spacing: 0.5px;">Lihat Preview Video</span>
      `;
      previewWrapper.appendChild(playBtn);

      // Add hover effects
      previewWrapper.onmouseenter = () => {
        previewWrapper.style.borderColor = "#ff6600";
        previewWrapper.style.boxShadow = "0 0 12px rgba(255, 102, 0, 0.15)";
        const glow = playBtn.querySelector(".play-icon-glow");
        if (glow) {
          glow.style.transform = "scale(1.12)";
          glow.style.background = "rgba(255, 102, 0, 0.25)";
          glow.style.boxShadow = "0 0 20px rgba(255, 102, 0, 0.6)";
        }
      };
      previewWrapper.onmouseleave = () => {
        previewWrapper.style.borderColor = "rgba(255, 102, 0, 0.15)";
        previewWrapper.style.boxShadow = "none";
        const glow = playBtn.querySelector(".play-icon-glow");
        if (glow) {
          glow.style.transform = "scale(1)";
          glow.style.background = "rgba(255, 102, 0, 0.1)";
          glow.style.boxShadow = "0 0 15px rgba(255, 102, 0, 0.3)";
        }
      };

      previewWrapper.onclick = async () => {
        // Replace wrapper content with actual video element on click
        previewWrapper.innerHTML = "";
        previewWrapper.style.cursor = "default";
        previewWrapper.style.borderColor = "transparent";
        previewWrapper.style.boxShadow = "none";
        previewWrapper.onmouseenter = null;
        previewWrapper.onmouseenter = null;
        previewWrapper.onmouseleave = null;

        const video = document.createElement("video");
        video.className = "result-card-video";
        video.style.width = "100%";
        video.style.height = "100%";
        video.style.borderRadius = "10px";
        video.controls = true;
        video.autoplay = true;

        // Streaming: langsung pakai URL proxy server (tidak ada cache Blob lokal).
        video.src = item.url;

        previewWrapper.appendChild(video);
      };

      const actions = document.createElement("div");
      actions.className = "result-card-actions";

      const download = document.createElement("a");
      download.className = "download";
      download.href = item.url;
      download.download = "";
      download.textContent = "⬇️ Download Video";

      const removeBtn = document.createElement("button");
      removeBtn.type = "button";
      removeBtn.className = "ghost result-remove-btn";
      removeBtn.setAttribute("data-remove-result", item.id);
      removeBtn.textContent = "Hapus";

      actions.append(download, removeBtn);
      card.append(previewWrapper, actions);
    }

    fragment.appendChild(card);
  });

  els.resultGallery.appendChild(fragment);
  els.result.hidden = false;
  startResultExpiryTimer();
}

function clearStoredResult() {
  stopResultExpiryTimer();
  localStorage.removeItem(RESULT_KEY);
  localStorage.removeItem(LEGACY_RESULT_KEY);
  if (els.resultGallery) {
    els.resultGallery.innerHTML = "";
  }
  if (els.result) {
    els.result.hidden = true;
  }
  if (els.resultRetention) {
    els.resultRetention.hidden = true;
    els.resultRetention.textContent = "";
  }
}

function removeStoredResult(id) {
  const next = readStoredResults().filter((item) => item.id !== id);
  writeStoredResults(next);

  if (activePolls.has(id)) {
    const abortCtrl = activePolls.get(id);
    if (abortCtrl) {
      abortCtrl.abort();
    }
    activePolls.delete(id);
  }

  renderStoredResults();
  updateGenerateBtn();
}

function restoreUploadsFromStorage() {
  // Restore image
  try {
    const imgData = JSON.parse(localStorage.getItem(UPLOAD_IMG_KEY));
    if (imgData && imgData.url) {
      // Only restore if saved within last 6 hours
      if (Date.now() - imgData.savedAt < 6 * 60 * 60 * 1000) {
        state.image.url = imgData.url;
        state.image.meta = imgData.meta || {};
        showPreview("image", imgData.url);
      } else {
        localStorage.removeItem(UPLOAD_IMG_KEY);
      }
    }
  } catch {}

  // Restore video
  try {
    const vidData = JSON.parse(localStorage.getItem(UPLOAD_VID_KEY));
    if (vidData && vidData.url) {
      if (Date.now() - vidData.savedAt < 6 * 60 * 60 * 1000) {
        state.video.url = vidData.url;
        state.video.meta = vidData.meta || {};
        showPreview("video", vidData.url);
      } else {
        localStorage.removeItem(UPLOAD_VID_KEY);
      }
    }
  } catch {}

  updateGenerateBtn();
}

function restoreResultFromStorage() {
  try {
    renderStoredResults();
  } catch {
    clearStoredResult();
  }
}

// ─── Active Generation State ─────────────────────────────
function saveActiveGeneration(data) {
  localStorage.setItem(ACTIVE_GEN_KEY, JSON.stringify({
    ...data,
    startedAt: Date.now(),
  }));
}

function clearActiveGeneration() {
  localStorage.removeItem(ACTIVE_GEN_KEY);
}

function getActiveGeneration() {
  try {
    const data = JSON.parse(localStorage.getItem(ACTIVE_GEN_KEY));
    if (data && data.startedAt) {
      // AFK-SAFE: Valid for up to 2 hours (matches max Weavy generation time)
      // Old TTL was 10 min which was too short for long Kling generations
      if (Date.now() - data.startedAt < 2 * 60 * 60 * 1000) {
        return data;
      }
    }
  } catch {}
  localStorage.removeItem(ACTIVE_GEN_KEY);
  return null;
}

async function startBackgroundPolling(entryId, accountId, batchId, runId, recipeId) {
  const abortCtrl = new AbortController();
  activePolls.set(entryId, abortCtrl);

  try {
    const videoUrl = await pollWeavyBatch(entryId, batchId || accountId, abortCtrl.signal);
    
    justFinishedIds.add(entryId);
    updateStoredResultEntry(entryId, {
      status: "completed",
      url: videoUrl,
      savedAt: Date.now(),
      expiresAt: Date.now() + RESULT_RETENTION_MS,
    });
    activePolls.delete(entryId);
    renderStoredResults();
    refreshPool();
    refreshVoucherStatus();
  } catch (err) {
    if (abortCtrl.signal.aborted) {
      console.log(`Polling ${entryId} aborted by user.`);
      return;
    }
    console.error(`Polling resume ${entryId} failed:`, err);
    updateStoredResultEntry(entryId, {
      status: "failed",
      error: sanitizeErrorForUser(err.message || "Gagal memuat video."),
    });
    activePolls.delete(entryId);
    renderStoredResults();
  } finally {
    updateGenerateBtn();
  }
}

async function resumeActiveGeneration() {
  const results = readStoredResults();
  const processingItems = results.filter(item => item.status === "processing");

  // AFK-SAFE: First, try to recover results from server that may have completed
  // while user was AFK. This handles the case where user closed browser for hours.
  for (const item of processingItems.slice()) {
    if (item.accountId && item.batchId) {
      try {
        const jobStatus = await fetch(`/api/jobs/status/${item.id}`, {
          headers: { "x-voucher-code": state.voucher }
        });
        if (jobStatus.ok) {
          const data = await jobStatus.json();
          if (data.status === "COMPLETED" && data.url) {
            // Server already finished — update local state immediately
            updateStoredResultEntry(item.id, {
              status: "completed",
              url: data.url,
              savedAt: Date.now(),
              expiresAt: Date.now() + RESULT_RETENTION_MS,
            });
            console.log(`[AFK-Recovery] Recovered completed job: ${item.id}`);
            continue; // Skip polling for this one
          }
          if (data.status === "FAILED") {
            updateStoredResultEntry(item.id, {
              status: "failed",
              error: sanitizeErrorForUser(data.error || "Generate gagal di server"),
            });
            console.log(`[AFK-Recovery] Job already failed: ${item.id}`);
            continue;
          }
        }
      } catch (err) {
        console.warn(`[AFK-Recovery] Could not check ${item.id}:`, err.message);
      }
    }
  }

  // Now re-poll any items still in processing state
  const stillProcessing = readStoredResults().filter(item => item.status === "processing");
  stillProcessing.forEach(item => {
    if (item.accountId && item.batchId) {
      startBackgroundPolling(item.id, item.accountId, item.batchId, item.runId, item.recipeId);
    } else {
      updateStoredResultEntry(item.id, {
        status: "failed",
        error: "Koneksi terputus saat halaman di-reload."
      });
    }
  });

  renderStoredResults();
  updateGenerateBtn();
}



// ═══════════════════════════════════════════════════════
// STATUS
// ═══════════════════════════════════════════════════════

function setStatus(text, type) {
  type = type || "";
  if (!text) { els.status.hidden = true; return; }
  els.status.hidden = false;
  els.status.textContent = text;
  els.status.className = "status " + type;
}

function updateGenerateBtn() {
  const ready = state.image.url && state.video.url
    && !state.image.uploading && !state.video.uploading;
  
  const activeList = readStoredResults().filter(r => r.status === "processing");
  const isLimitReached = activeList.length >= 3;

  els.generateBtn.disabled = !ready || isLimitReached;
  
  if (isLimitReached) {
    els.generateBtn.textContent = "⏳ Maksimal 3 generate aktif secara bersamaan";
    els.generateBtn.style.background = "var(--border)";
  } else {
    els.generateBtn.textContent = "🚀 Generate Video";
    els.generateBtn.style.background = "";
  }
}

// ═══════════════════════════════════════════════════════
// FILE UPLOAD
// ═══════════════════════════════════════════════════════

async function uploadFile(kind, file) {
  state[kind].uploading = true;
  updateGenerateBtn();
  
  const dz = kind === "image" ? els.imageDz : els.videoDz;
  const originalContent = dz.innerHTML;
  
  // Set upload loading state in the dropzone
  dz.classList.add("uploading");
  dz.innerHTML = `
    <div class="dz-loader">
      <div class="spinner"></div>
      <div style="font-weight: 600; font-size: 0.9rem; color: var(--accent);">Mengunggah ${kind === "image" ? "Gambar" : "Video"}...</div>
      <div style="font-size: 0.75rem; color: var(--muted); max-width: 250px; text-align: center; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">${file.name}</div>
    </div>
  `;

  setStatus(`Sedang mengunggah ${kind === "image" ? "referensi gambar" : "referensi video"}: ${file.name}...`, "warn");

  const endpoint = "/api/upload";
  const fd = new FormData();
  fd.append("file", file);

  try {
    const r = await fetch(endpoint, {
      method: "POST",
      headers: { "x-voucher-code": state.voucher },
      body: fd
    });
    
    const data = await r.json().catch(() => ({}));
    if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`);

    let url;
    const asset = data.asset || data.file || data;
    url = asset.url || asset.secureUrl || asset.secure_url;
    if (!url) throw new Error("Upload sukses tapi URL tidak ditemukan");
    state[kind].meta = {
      width: asset.width, height: asset.height,
      duration: asset.duration, fps: asset.fps, hasAudio: asset.hasAudio,
      thumbnailUrl: asset.thumbnailUrl,
      publicId: asset.publicId || asset.public_id,
      id: asset.id, name: asset.name || file.name,
    };

    state[kind].url = url;
    showPreview(kind, url);
    saveUploadToStorage(kind);
    setStatus(`${kind === "image" ? "Gambar" : "Video"} berhasil diunggah!`, "done");
    setTimeout(() => setStatus(""), 2000);
  } catch (err) {
    state[kind].url = "";
    state[kind].meta = {};
    
    const errMsg = `❌ Gagal mengunggah ${kind === "image" ? "gambar" : "video"}: ${sanitizeErrorForUser(err.message)}. \n⚠️ Kemungkinan besar Status Server sedang 0 (tidak ada akun aktif di pool) atau server offline. Segera laporkan/hubungi Admin!`;
    setStatus(errMsg, "error");
    
    // Show an error status in the dropzone too!
    dz.innerHTML = `
      <div style="color: var(--accent); text-align: center; padding: 1rem;">
        <span style="font-size: 1.5rem; display: block; margin-bottom: 0.5rem;">⚠️</span>
        <strong style="font-size: 0.85rem; display: block;">Gagal Upload!</strong>
        <span style="font-size: 0.72rem; color: var(--muted); display: block; margin-top: 4px; line-height: 1.3;">Akun pool kosong (Status 0) atau offline. Laporkan ke Admin.</span>
      </div>
    `;
    
    setTimeout(() => {
      if (state[kind].url === "") {
        dz.innerHTML = originalContent;
        dz.classList.remove("uploading");
      }
    }, 6000);
    return;
  } finally {
    if (state[kind].url !== "") {
      dz.innerHTML = originalContent;
      dz.classList.remove("uploading");
    }
    state[kind].uploading = false;
    updateGenerateBtn();
  }
}

function showPreview(kind, url) {
  if (kind === "image") {
    els.imagePreview.src = url;
    els.imagePreviewBox.hidden = false;
    els.imageDz.classList.add("compact");
  } else {
    els.videoPreview.src = url;
    els.videoPreviewBox.hidden = false;
    els.videoDz.classList.add("compact");
  }
}

function clearPreview(kind) {
  state[kind] = { url: "", meta: {}, uploading: false };
  removeUploadFromStorage(kind);
  if (kind === "image") {
    if (els.imagePreview) els.imagePreview.removeAttribute("src");
    if (els.imagePreviewBox) els.imagePreviewBox.hidden = true;
    if (els.imageDz) els.imageDz.classList.remove("compact");
    if (els.imageFile) els.imageFile.value = "";
  } else {
    if (els.videoPreview) els.videoPreview.removeAttribute("src");
    if (els.videoPreviewBox) els.videoPreviewBox.hidden = true;
    if (els.videoDz) els.videoDz.classList.remove("compact");
    if (els.videoFile) els.videoFile.value = "";
  }
  updateGenerateBtn();
}

function bindDropzone(kind) {
  const dz = kind === "image" ? els.imageDz : els.videoDz;
  const input = kind === "image" ? els.imageFile : els.videoFile;
  const acceptPrefix = kind === "image" ? "image/" : "video/";

  dz.addEventListener("click", (e) => {
    if (e.target.classList.contains("remove-btn")) return;
    if (e.target.tagName === "VIDEO" || e.target.tagName === "IMG") return;
    input.click();
  });
  input.addEventListener("change", (e) => {
    const file = e.target.files[0];
    if (file && file.type.startsWith(acceptPrefix)) uploadFile(kind, file);
  });
  dz.addEventListener("dragover", (e) => { e.preventDefault(); dz.classList.add("dragging"); });
  dz.addEventListener("dragleave", () => dz.classList.remove("dragging"));
  dz.addEventListener("drop", (e) => {
    e.preventDefault();
    dz.classList.remove("dragging");
    const file = e.dataTransfer.files[0];
    if (file && file.type.startsWith(acceptPrefix)) uploadFile(kind, file);
  });
}

document.querySelectorAll(".remove-btn").forEach((btn) => {
  btn.addEventListener("click", (e) => {
    e.stopPropagation();
    clearPreview(btn.dataset.target);
  });
});

function retryStoredResult(id) {
  const results = readStoredResults();
  const item = results.find(r => r.id === id);
  if (!item || !item.payload) return;

  // Reset card state to processing
  item.status = "processing";
  item.progress = 5;
  item.statusText = "Menghubungkan kembali ke API server...";
  delete item.accountId;
  delete item.batchId;
  delete item.runId;
  delete item.recipeId;
  delete item.error;

  writeStoredResults(results);
  renderStoredResults();
  updateGenerateBtn();

  startBackgroundGeneration(id, item.payload);
}

if (els.resultGallery) {
  els.resultGallery.addEventListener("click", (e) => {
    const removeBtn = e.target.closest("[data-remove-result]");
    if (removeBtn) {
      removeStoredResult(removeBtn.getAttribute("data-remove-result"));
      return;
    }
    const retryBtn = e.target.closest("[data-retry-result]");
    if (retryBtn) {
      retryStoredResult(retryBtn.getAttribute("data-retry-result"));
      return;
    }
  });
}

bindDropzone("image");
bindDropzone("video");

// ═══════════════════════════════════════════════════════
// PREMIUM PROGRESS ANIMATOR (REAL-TIME & SMOOTH)
// ═══════════════════════════════════════════════════════

let progressInterval = null;
let progressTarget = 0;
let progressCurrent = 0;

function startProgressSimulation() {
  stopProgressSimulation();
  progressCurrent = 0;
  progressTarget = 5; // Start at 5%
  updateProgressUI(progressCurrent, "Menghubungkan ke API server...");
  
  if (els.progressContainer) els.progressContainer.hidden = false;

  // Reset progress bar styling (e.g. from any previous errors)
  const progressBarFill = document.getElementById("progress-bar-fill");
  if (progressBarFill) {
    progressBarFill.style.background = "";
    progressBarFill.style.boxShadow = "";
  }

  // Smoothly interpolate current toward target
  progressInterval = setInterval(() => {
    if (progressCurrent < progressTarget) {
      const diff = progressTarget - progressCurrent;
      const step = Math.max(0.1, diff * 0.12); // Smooth logarithmic easing
      progressCurrent = parseFloat((progressCurrent + step).toFixed(1));
      if (progressCurrent > progressTarget) progressCurrent = progressTarget;
      updateProgressUI(progressCurrent);
    }
  }, 100);
}

function setProgressTarget(target, statusText) {
  progressTarget = Math.min(99, Math.max(progressTarget, target));
  if (statusText) {
    updateProgressUI(progressCurrent, statusText);
  }
}

function updateProgressUI(pct, statusText) {
  const percentText = Math.floor(pct) + "%";
  const progressBarFill = document.getElementById("progress-bar-fill");
  const progressPercent = document.getElementById("progress-percent");
  const progressStatusText = document.getElementById("progress-status-text");

  if (progressBarFill) progressBarFill.style.width = percentText;
  if (progressPercent) progressPercent.textContent = percentText;
  if (progressStatusText && statusText) {
    progressStatusText.textContent = statusText;
  }
}

function stopProgressSimulation() {
  if (progressInterval) {
    clearInterval(progressInterval);
    progressInterval = null;
  }
}

function completeProgress(statusText) {
  stopProgressSimulation();
  progressCurrent = 100;
  progressTarget = 100;
  updateProgressUI(100, statusText || "Selesai ✓");
  setTimeout(() => {
    if (els.progressContainer) els.progressContainer.hidden = true;
  }, 1500);
}

function failProgress(statusText) {
  stopProgressSimulation();
  updateProgressUI(progressCurrent, statusText || "Gagal!");
  const progressBarFill = document.getElementById("progress-bar-fill");
  if (progressBarFill) {
    progressBarFill.style.background = "var(--accent)";
    progressBarFill.style.boxShadow = "0 0 10px var(--accent)";
  }
  setTimeout(() => {
    if (els.progressContainer) els.progressContainer.hidden = true;
  }, 3000);
}



// Helper to safely delay with AbortSignal support, handling missing addEventListener
function delayWithSignal(ms, abortSignal) {
  return new Promise((resolve, reject) => {
    if (abortSignal && abortSignal.aborted) {
      return reject(new Error("Aborted"));
    }

    const timer = setTimeout(() => {
      cleanup();
      resolve();
    }, ms);

    const onAbort = () => {
      clearTimeout(timer);
      cleanup();
      reject(new Error("Aborted"));
    };

    let pollInterval = null;

    function cleanup() {
      if (abortSignal && typeof abortSignal.removeEventListener === 'function') {
        abortSignal.removeEventListener("abort", onAbort);
      }
      if (pollInterval) {
        clearInterval(pollInterval);
        pollInterval = null;
      }
    }

    if (abortSignal) {
      if (typeof abortSignal.addEventListener === 'function') {
        abortSignal.addEventListener("abort", onAbort);
      } else {
        pollInterval = setInterval(() => {
          if (abortSignal.aborted) {
            onAbort();
          }
        }, 100);
      }
    }
  });
}

async function pollWeavyBatch(entryId, jobId, abortSignal) {
  const maxAttempts = 300; // 300 * 3 seconds = 15 minutes (matches server-side background poll timeout)
  const estimatedMaxTimeSec = 85; // Typical Kling generation time via Weavy is ~85 seconds
  let consecutiveNetworkErrors = 0;
  const maxConsecutiveNetworkErrors = 10; // Try for up to 30 seconds

  for (let i = 0; i < maxAttempts; i++) {
    if (abortSignal && abortSignal.aborted) throw new Error("Aborted");

    // Wait 3 seconds
    await delayWithSignal(3000, abortSignal);

    if (abortSignal && abortSignal.aborted) throw new Error("Aborted");

    const url = `/api/poll/${jobId}`;
    
    let res;
    try {
      res = await fetch(url, {
        headers: {
          "x-voucher-code": state.voucher
        },
        signal: (abortSignal && typeof abortSignal.addEventListener === 'function') ? abortSignal : undefined
      });
      consecutiveNetworkErrors = 0; // Reset on successful response
    } catch (fetchErr) {
      if (abortSignal && abortSignal.aborted) throw fetchErr;

      consecutiveNetworkErrors++;
      console.warn(`Polling network error (${consecutiveNetworkErrors}/${maxConsecutiveNetworkErrors}):`, fetchErr.message);

      if (consecutiveNetworkErrors >= maxConsecutiveNetworkErrors) {
        throw new Error("Koneksi internet terputus. Silakan klik Coba Lagi.");
      }

      updateStoredResultEntry(entryId, {
        statusText: `⚠️ Masalah koneksi. Menghubungkan kembali (${consecutiveNetworkErrors}/${maxConsecutiveNetworkErrors})...`
      });
      renderStoredResults();
      continue; // Retry in next loop iteration
    }

    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      if (i > 5) throw new Error(data.error || "Polling failed");
      continue;
    }

    // Use progress percentage calculated by the server background worker if available, fallback to client estimation
    const percent = typeof data.progress === "number" ? data.progress : Math.min(98, 15 + Math.floor((((i + 1) * 3) / estimatedMaxTimeSec) * 80));

    let statusMsg = "Menunggu antrian server...";
    if (data.status) {
      const st = String(data.status).toUpperCase();
      if (st === "FAILED" || st === "CANCELLED") {
        throw new Error(data.error || `Task ${st.toLowerCase()} di sistem.`);
      }
      if (st === "PENDING" || st === "QUEUED") {
        statusMsg = "Mengantre di server...";
      } else if (st === "RUNNING") {
        if (percent > 85) statusMsg = "Hampir selesai, memproses audio video...";
        else if (percent > 55) statusMsg = "Menyinkronkan gerakan karakter...";
        else statusMsg = "GPU sedang merender frame video...";
      } else {
        statusMsg = `Status: ${data.status}...`;
      }
    } else {
      if (percent > 85) statusMsg = "Hampir selesai, memproses audio video...";
      else if (percent > 55) statusMsg = "Menyinkronkan gerakan karakter...";
      else statusMsg = "GPU sedang merender frame video...";
    }

    updateStoredResultEntry(entryId, {
      progress: percent,
      statusText: statusMsg
    });
    renderStoredResults();

    if (data.found && data.url) return data.url;
  }
  throw new Error("Polling timeout (15 menit).");
}

async function generateViaWeavy(entryId, payload, abortSignal) {
  const failedAccountIds = [];
  const maxAttempts = 3;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    if (abortSignal && abortSignal.aborted) throw new Error("Aborted");

    let currentAccountId = null;
    try {
      if (attempt > 1) {
        updateStoredResultEntry(entryId, {
          progress: 5,
          statusText: `Gagal di akun sebelumnya. Mencoba ulang dengan akun lain (Percobaan ${attempt}/${maxAttempts})...`
        });
        renderStoredResults();
        
        await delayWithSignal(3000, abortSignal);
      }

      const body = {
        imageUrl: payload.imageUrl,
        imageMeta: payload.imageMeta,
        videoUrl: payload.videoUrl,
        videoMeta: payload.videoMeta,
        prompt: payload.prompt,
        modelType: payload.modelType,
        version: payload.version,
        characterOrientation: payload.characterOrientation,
        keepOriginalSound: payload.keepOriginalSound,
        numberOfRuns: 1,
        excludeIds: failedAccountIds,
      };

      updateStoredResultEntry(entryId, {
        progress: 8,
        statusText: attempt === 1 
          ? "Menyiapkan material dan menghubungi server..." 
          : `Menghubungi server dengan akun baru (Percobaan ${attempt})...`
      });
      renderStoredResults();

      let res;
      let networkRetries = 0;
      while (networkRetries < 3) {
        if (abortSignal && abortSignal.aborted) throw new Error("Aborted");
        try {
          res = await fetch("/api/generate", {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              "x-voucher-code": state.voucher
            },
            body: JSON.stringify(body),
            signal: (abortSignal && typeof abortSignal.addEventListener === 'function') ? abortSignal : undefined
          });
          break; // Success
        } catch (netErr) {
          if (abortSignal && abortSignal.aborted) throw netErr;
          networkRetries++;
          if (networkRetries >= 3) {
            throw new Error("Gagal menghubungi server. Harap periksa koneksi internet Anda.");
          }
          updateStoredResultEntry(entryId, {
            statusText: `⚠️ Masalah jaringan. Mencoba menghubungi server (${networkRetries}/3)...`
          });
          renderStoredResults();
          await delayWithSignal(3000, abortSignal);
        }
      }

      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        if (data.accountId) {
          failedAccountIds.push(data.accountId);
        }
        throw new Error(typeof data.error === "string" ? data.error : JSON.stringify(data.error || data));
      }

      const { jobId, _accountId: accountId } = data;
      currentAccountId = accountId;

      if (!jobId) throw new Error("Response tidak lengkap dari API server.");

      updateStoredResultEntry(entryId, {
        accountId: jobId,
        batchId: jobId,
        progress: 15,
        statusText: "Mengantre untuk render video..."
      });
      renderStoredResults();

      // Clear uploads only after success
      clearAllUploadStorage();
      clearPreview("image");
      clearPreview("video");

      const videoUrl = await pollWeavyBatch(entryId, jobId, abortSignal);
      return videoUrl;

    } catch (err) {
      console.warn(`Percobaan ${attempt} gagal:`, err.message);
      if (currentAccountId) {
        failedAccountIds.push(currentAccountId);
      }
      if (attempt === maxAttempts || (abortSignal && abortSignal.aborted)) {
        throw err;
      }
    }
  }
}

async function startBackgroundGeneration(entryId, payload) {
  const abortCtrl = new AbortController();
  activePolls.set(entryId, abortCtrl);

  try {
    const videoUrl = await generateViaWeavy(entryId, payload, abortCtrl.signal);

    justFinishedIds.add(entryId);
    const savedAt = Date.now();
    updateStoredResultEntry(entryId, {
      status: "completed",
      url: videoUrl,
      savedAt,
      expiresAt: savedAt + RESULT_RETENTION_MS,
    });
    activePolls.delete(entryId);

    renderStoredResults();
    refreshPool();
    refreshVoucherStatus();
  } catch (err) {
    if (abortCtrl.signal.aborted) {
      console.log(`Generation ${entryId} aborted by user.`);
      return;
    }
    console.error(`Generation ${entryId} failed:`, err);
    updateStoredResultEntry(entryId, {
      status: "failed",
      error: sanitizeErrorForUser(err.message || "Gagal membuat video."),
    });
    activePolls.delete(entryId);
    renderStoredResults();
  } finally {
    updateGenerateBtn();
  }
}

$("generate-btn").addEventListener("click", async () => {
  if (!state.image.url || !state.video.url) {
    return setStatus("Image dan Video wajib di-upload.", "error");
  }

  setStatus(""); // Clear text status

  const payload = {
    imageUrl: state.image.url,
    imageMeta: state.image.meta,
    videoUrl: state.video.url,
    videoMeta: state.video.meta,
    prompt: els.prompt.value,
    modelType: els.modelType.value,
    version: els.modelVersion.value,
    characterOrientation: els.orientation.value,
    keepOriginalSound: els.keepSound.checked,
  };

  const entryId = `result_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  
  addProcessingResultToStorage(entryId);
  renderStoredResults();
  updateGenerateBtn();

  startBackgroundGeneration(entryId, payload);
});

// ═══════════════════════════════════════════════════════
// BOOTSTRAP
// ═══════════════════════════════════════════════════════

async function refreshPool() {
  try {
    const r = await fetch("/api/config");
    const cfg = await r.json();
    state.recipeId = cfg.defaultRecipeId;
    state.persistMode = cfg.persistMode || "server";
    state.assetTtlMs = cfg.assetTtlMs || 24 * 60 * 60 * 1000;
    const txt = `${cfg.availableAccounts}/${cfg.totalAccounts} aktif`;
    if (els.poolInfo) els.poolInfo.textContent = txt;
    if (els.poolInfoFooter) els.poolInfoFooter.textContent = txt;

    // Handle maintenance mode
    handleMaintenanceState(cfg.maintenance);

    if (cfg.availableAccounts === 0) {
      if (els.poolBanner) {
        els.poolBanner.textContent = "⚠️ Belum ada akun aktif di pool. Hubungi admin.";
        els.poolBanner.hidden = false;
      }
    } else {
      if (els.poolBanner) els.poolBanner.hidden = true;
    }
  } catch {}
}

function handleMaintenanceState(maintenance) {
  const overlay = document.getElementById("maintenance-overlay");
  const titleEl = document.getElementById("maintenance-title");
  const descEl = document.getElementById("maintenance-description");
  const reasonEl = document.getElementById("maintenance-reason");
  const durationTextEl = document.getElementById("maintenance-duration-text");

  if (!overlay) return;

  if (maintenance && maintenance.active) {
    overlay.style.display = "flex";
    if (titleEl) titleEl.textContent = maintenance.title || "Server Sedang Pemeliharaan";
    if (descEl) descEl.textContent = maintenance.description || "Kami sedang melakukan peningkatan sistem. Generator diistirahatkan sementara untuk memastikan kestabilan layanan.";
    if (reasonEl) reasonEl.textContent = maintenance.reason || "Pemeliharaan sistem berkala";

    // Clear existing interval to avoid duplicates
    if (maintenanceInterval) {
      clearInterval(maintenanceInterval);
      maintenanceInterval = null;
    }

    if (maintenance.expiresAt) {
      const updateCountdown = () => {
        const ms = new Date(maintenance.expiresAt).getTime() - Date.now();
        if (ms <= 0) {
          if (durationTextEl) durationTextEl.textContent = "Selesai. Memproses...";
          clearInterval(maintenanceInterval);
          maintenanceInterval = null;
          // Refresh config in 2 seconds to check if maintenance is disabled
          setTimeout(refreshPool, 2000);
        } else {
          const totalSecs = Math.floor(ms / 1000);
          const hrs = Math.floor(totalSecs / 3600);
          const mins = Math.floor((totalSecs % 3600) / 60);
          const secs = totalSecs % 60;

          let cdText = "";
          if (hrs > 0) {
            cdText += `${hrs} jam `;
          }
          cdText += `${mins} menit ${secs} detik lagi`;
          if (durationTextEl) {
            durationTextEl.textContent = cdText;
          }
        }
      };

      updateCountdown();
      maintenanceInterval = setInterval(updateCountdown, 1000);
    } else {
      if (durationTextEl) {
        durationTextEl.textContent = maintenance.duration || "Ditunggu saja";
      }
    }
  } else {
    overlay.style.display = "none";
    if (maintenanceInterval) {
      clearInterval(maintenanceInterval);
      maintenanceInterval = null;
    }
  }
}

function maskVoucherCode(code) {
  if (!code) return "";
  if (code.length <= 4) {
    return code.substring(0, 1) + "***" + code.slice(-1);
  }
  return code.substring(0, 2) + "*".repeat(code.length - 4) + code.slice(-2);
}

async function refreshVoucherStatus() {
  if (!state.voucher) {
    if (els.voucherInfoCard) els.voucherInfoCard.style.display = "none";
    return;
  }

  try {
    const r = await fetch("/api/auth/voucher-status", {
      headers: {
        "x-voucher-code": state.voucher
      }
    });
    if (!r.ok) {
      if (r.status === 401) {
        logoutVoucher();
      }
      return;
    }

    const data = await r.json();
    if (data.ok) {
      if (els.userVoucherCode) els.userVoucherCode.textContent = maskVoucherCode(data.code);
      if (els.userVoucherTier) {
        els.userVoucherTier.textContent = data.tier.toUpperCase();
        els.userVoucherTier.style.color = data.tier === "vip" ? "#fbbf24" : (data.tier === "pro" ? "#60a5fa" : "var(--warn)");
      }
      if (els.userVoucherQuota) {
        els.userVoucherQuota.textContent = `${data.usage} / ${data.dailyLimit} kali digunakan hari ini`;
      }
      if (els.userVoucherRemaining) {
        const days = data.remainingDays;
        let color = "var(--success)";
        let text = `${days} hari`;
        if (days <= 0) { color = "#ef4444"; text = "Kadaluarsa"; }
        else if (days <= 2) { color = "#f97316"; text = `${days} hari (Segera perpanjang!)`; }
        else if (days <= 7) { color = "#fbbf24"; text = `${days} hari`; }
        els.userVoucherRemaining.style.color = color;
        els.userVoucherRemaining.textContent = text;
      }
      if (els.voucherInfoCard) els.voucherInfoCard.style.display = "block";

      // Trigger expiring popup jika remainingDays <= 2 dan >= 0
      if (data.remainingDays >= 0 && data.remainingDays <= 2) {
        showExpiringPopup(data);
      }
    }
  } catch (err) {
    console.error("Gagal refresh voucher status:", err);
  }
}

function logoutVoucher() {
  state.voucher = "";
  state.tier = "";
  localStorage.removeItem(VOUCHER_KEY);
  localStorage.removeItem(TIER_KEY);
  
  if (els.voucherInfoCard) els.voucherInfoCard.style.display = "none";
  
  els.modelType.disabled = false;
  els.modelVersion.disabled = false;
  const hintEl = $("tier-lock-hint");
  if (hintEl) hintEl.remove();

  clearPreview("image");
  clearPreview("video");
  els.prompt.value = "";
  
  showLoginModal();
}

// ─── Membership Expiring Popup ──────────────────────────

function showExpiringPopup(data) {
  // Cek snooze: jika sudah di-snooze dan belum lewat 24 jam, skip
  const snoozeUntil = localStorage.getItem(EXPIRING_SNOOZE_KEY);
  if (snoozeUntil && Date.now() < parseInt(snoozeUntil, 10)) {
    return;
  }

  const msg = `Masa aktif membership Anda tersisa <strong style="color:#f97316;">${data.remainingDays} hari</strong> lagi.`;
  const msgEl = document.getElementById("expiring-popup-msg");
  if (msgEl) msgEl.innerHTML = msg;

  if (els.expiringPopup) {
    els.expiringPopup.style.display = "flex";
  }
}

// Setup expiring popup event listeners
document.addEventListener("DOMContentLoaded", function() {
  if (els.expiringSnoozeBtn) {
    els.expiringSnoozeBtn.addEventListener("click", function() {
      // Snooze 24 jam
      localStorage.setItem(EXPIRING_SNOOZE_KEY, String(Date.now() + 24 * 60 * 60 * 1000));
      if (els.expiringPopup) els.expiringPopup.style.display = "none";
    });
  }
  if (els.expiringCloseBtn) {
    els.expiringCloseBtn.addEventListener("click", function() {
      if (els.expiringPopup) els.expiringPopup.style.display = "none";
    });
  }
});


refreshPool();
refreshVoucherStatus();
setInterval(refreshPool, 30_000);
setInterval(refreshVoucherStatus, 30_000);
updateGenerateBtn();

// Restore uploads and last result from localStorage on page load
restoreUploadsFromStorage();
restoreResultFromStorage();

// Resume active generation if page was reloaded during processing
resumeActiveGeneration();

// ─── Member Voucher Portal (Overlay) & Tier Restrictions ─────────────────────

function showLoginModal() {
  if (els.loginModal) {
    els.loginModal.style.display = "flex";
    els.voucherInput.focus();
  }
}

function hideLoginModal() {
  if (els.loginModal) els.loginModal.style.display = "none";
}

function showModalStatus(text, type = "error") {
  if (!els.modalStatus) return;
  if (!text) { els.modalStatus.hidden = true; return; }
  els.modalStatus.hidden = false;
  els.modalStatus.textContent = text;
  els.modalStatus.className = "status " + type;
}

async function submitVoucher() {
  const code = els.voucherInput.value.trim().toUpperCase();
  if (!code) {
    showModalStatus("Masukkan kode voucher VIP Anda.");
    return;
  }
  
  showModalStatus("Memverifikasi...", "warn");
  try {
    const r = await fetch("/api/auth/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ voucherCode: code })
    });
    const data = await r.json();
    if (!r.ok) throw new Error(data.error || "Aktivasi gagal");
    
    // Save state
    state.voucher = code;
    state.tier = data.tier;
    localStorage.setItem(VOUCHER_KEY, code);
    localStorage.setItem(TIER_KEY, data.tier);
    
    hideLoginModal();
    applyTierLock();
    refreshVoucherStatus();
    showModalStatus("");
    
    if (data.promo) {
      showPromoPopup(data.promo);
    }
  } catch (err) {
    showModalStatus("Gagal: " + sanitizeErrorForUser(err.message), "error");
  }
}

function applyTierLock() {
  const hintEl = $("tier-lock-hint");
  if (hintEl) hintEl.remove();
  if (els.modelType) els.modelType.disabled = false;
  if (els.modelVersion) els.modelVersion.disabled = false;
}

// Bind Submit Button
if (els.submitVoucherBtn) {
  els.submitVoucherBtn.addEventListener("click", submitVoucher);
}
if (els.logoutVoucherBtn) {
  els.logoutVoucherBtn.addEventListener("click", logoutVoucher);
}
if (els.voucherInput) {
  els.voucherInput.addEventListener("keydown", (e) => {
    if (e.key === "Enter") submitVoucher();
  });
}

// Intercept Click Events to block unauthorized usage
if (els.interactiveContainer) {
  els.interactiveContainer.addEventListener("click", (e) => {
    if (!state.voucher) {
      e.preventDefault();
      e.stopPropagation();
      showLoginModal();
    }
  }, true); // useCapture is true
}

// Initialize Tier Lock check on page load
applyTierLock();

function showPromoPopup(promo) {
  const overlay = document.createElement("div");
  overlay.id = "promo-popup-overlay";
  overlay.style.position = "fixed";
  overlay.style.top = "0";
  overlay.style.left = "0";
  overlay.style.width = "100vw";
  overlay.style.height = "100vh";
  overlay.style.backgroundColor = "rgba(0, 0, 0, 0.8)";
  overlay.style.backdropFilter = "blur(12px)";
  overlay.style.display = "flex";
  overlay.style.justifyContent = "center";
  overlay.style.alignItems = "center";
  overlay.style.zIndex = "99999";
  overlay.style.opacity = "0";
  overlay.style.transition = "opacity 0.4s ease";

  const card = document.createElement("div");
  card.id = "promo-popup-card";
  card.style.width = "90%";
  card.style.maxWidth = "500px";
  card.style.backgroundColor = "#111111";
  card.style.border = "1px solid rgba(255, 102, 0, 0.4)";
  card.style.borderRadius = "24px";
  card.style.padding = "30px";
  card.style.boxShadow = "0 15px 50px rgba(255, 102, 0, 0.2)";
  card.style.textAlign = "center";
  card.style.transform = "scale(0.85)";
  card.style.transition = "transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)";
  card.style.color = "#ffffff";
  card.style.fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";

  const title = document.createElement("h2");
  title.textContent = promo.title || "⚡ PROMO SPESIAL ⚡";
  title.style.margin = "0 0 15px 0";
  title.style.fontSize = "22px";
  title.style.fontWeight = "800";
  title.style.letterSpacing = "1.5px";
  title.style.background = "linear-gradient(45deg, #ff6600, #ffaa00)";
  title.style.webkitBackgroundClip = "text";
  title.style.webkitTextFillColor = "transparent";
  card.appendChild(title);

  if (promo.mediaUrl) {
    const mediaContainer = document.createElement("div");
    mediaContainer.style.width = "100%";
    mediaContainer.style.height = "auto";
    mediaContainer.style.maxHeight = "480px";
    mediaContainer.style.borderRadius = "16px";
    mediaContainer.style.overflow = "hidden";
    mediaContainer.style.marginBottom = "20px";
    mediaContainer.style.backgroundColor = "#080808";
    mediaContainer.style.display = "flex";
    mediaContainer.style.justifyContent = "center";
    mediaContainer.style.alignItems = "center";
    mediaContainer.style.border = "1px solid rgba(255, 255, 255, 0.08)";

    if (promo.mediaType === "video") {
      const video = document.createElement("video");
      video.src = promo.mediaUrl;
      video.autoplay = true;
      video.controls = true;
      video.muted = true;
      video.loop = true;
      video.style.width = "100%";
      video.style.maxHeight = "480px";
      video.style.objectFit = "contain";
      mediaContainer.appendChild(video);
    } else {
      const img = document.createElement("img");
      img.src = promo.mediaUrl;
      img.style.width = "100%";
      img.style.maxHeight = "480px";
      img.style.objectFit = "contain";
      mediaContainer.appendChild(img);
    }
    card.appendChild(mediaContainer);
  }

  const desc = document.createElement("div");
  desc.innerHTML = formatPromoDescription(promo.description);
  desc.style.fontSize = "15px";
  desc.style.lineHeight = "1.6";
  desc.style.color = "#e5e5e5";
  desc.style.marginBottom = "25px";
  desc.style.maxHeight = "200px";
  desc.style.overflowY = "auto";
  desc.style.padding = "0 10px";
  card.appendChild(desc);

  const closeBtn = document.createElement("button");
  closeBtn.textContent = promo.btnText || "KLAIM SEKARANG";
  closeBtn.style.width = "100%";
  closeBtn.style.padding = "14px 20px";
  closeBtn.style.fontSize = "16px";
  closeBtn.style.fontWeight = "700";
  closeBtn.style.color = "#ffffff";
  closeBtn.style.background = "linear-gradient(135deg, #ff6600, #ff4500)";
  closeBtn.style.border = "none";
  closeBtn.style.borderRadius = "30px";
  closeBtn.style.cursor = "pointer";
  closeBtn.style.boxShadow = "0 4px 15px rgba(255, 102, 0, 0.35)";
  closeBtn.style.transition = "transform 0.2s ease, box-shadow 0.2s ease";

  closeBtn.onmouseover = () => {
    closeBtn.style.transform = "translateY(-2px)";
    closeBtn.style.boxShadow = "0 6px 20px rgba(255, 102, 0, 0.55)";
  };
  closeBtn.onmouseout = () => {
    closeBtn.style.transform = "translateY(0)";
    closeBtn.style.boxShadow = "0 4px 15px rgba(255, 102, 0, 0.35)";
  };

  const closePopup = () => {
    overlay.style.opacity = "0";
    card.style.transform = "scale(0.85)";
    setTimeout(() => {
      overlay.remove();
    }, 400);
  };

  closeBtn.onclick = () => {
    if (promo.btnLink && promo.btnLink.trim() !== "") {
      try {
        let url = promo.btnLink.trim();
        if (!/^https?:\/\//i.test(url)) {
          url = "https://" + url;
        }
        window.open(url, "_blank");
      } catch (err) {
        console.error("Gagal membuka link aksi:", err);
      }
    }
    closePopup();
  };
  card.appendChild(closeBtn);
  overlay.appendChild(card);
  document.body.appendChild(overlay);

  setTimeout(() => {
    overlay.style.opacity = "1";
    card.style.transform = "scale(1)";
  }, 50);
}

function formatPromoDescription(text) {
  if (!text) return "";
  const highlightWords = [
    "VIP", "PRO", "GRATIS", "FREE", "RENDER", "LIMIT", 
    "TANPA BATAS", "DISKON", "BONUS", "100%", "UNLIMITED",
    "SPESIAL", "KLAIM", "AKTIF", "SUKSES", "VOUCHER"
  ];
  const lines = text.split("\n").map(l => l.trim()).filter(l => l.length > 0);
  if (lines.length === 0) return "";
  let html = "";
  let headerProcessed = false;
  let subheaderProcessed = false;
  let listItems = [];
  
  function highlightText(t) {
    let formatted = t;
    highlightWords.forEach(word => {
      const escapedWord = word.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
      const regex = new RegExp(`\\b${escapedWord}\\b`, "gi");
      formatted = formatted.replace(regex, (match) => `<strong style="color: #ffaa00; font-weight: 800;">${match}</strong>`);
    });
    return formatted;
  }

  for (let i = 0; i < lines.length; i++) {
    let line = lines[i];
    if (line.startsWith("-") || line.startsWith("*") || line.startsWith("•") || line.startsWith("✓")) {
      const cleanLine = line.replace(/^[-*•✓]\s*/, "");
      listItems.push(highlightText(cleanLine));
    } else {
      if (listItems.length > 0) {
        html += renderList(listItems);
        listItems = [];
      }
      if (!headerProcessed) {
        html += `<h3 style="margin: 0 0 8px 0; font-size: 17px; font-weight: 800; color: #ffffff; letter-spacing: 0.5px; text-transform: uppercase; text-shadow: 0 2px 4px rgba(0,0,0,0.5);">${highlightText(line)}</h3>`;
        headerProcessed = true;
      } else if (!subheaderProcessed && lines.length > 2) {
        html += `<p style="margin: 0 0 12px 0; font-size: 13.5px; font-weight: 600; color: #b3b3b3; line-height: 1.4;">${highlightText(line)}</p>`;
        subheaderProcessed = true;
      } else {
        html += `<p style="margin: 0 0 10px 0; font-size: 13px; font-weight: 500; color: #cccccc; line-height: 1.5; text-align: center;">${highlightText(line)}</p>`;
      }
    }
  }
  if (listItems.length > 0) {
    html += renderList(listItems);
  }
  return html;
}

function renderList(items) {
  let listHtml = `<div style="text-align: left; display: inline-block; max-width: 100%; margin: 0 auto 12px auto;"><ul style="list-style: none; padding: 0; margin: 0;">`;
  items.forEach(item => {
    listHtml += `
      <li style="margin-bottom: 6px; display: flex; align-items: flex-start; gap: 6px; font-size: 13px; color: #e5e5e5; line-height: 1.4;">
        <span style="color: #ffaa00; font-size: 13px; flex-shrink: 0; margin-top: 1px;">⚡</span>
        <span>${item}</span>
      </li>`;
  });
  listHtml += `</ul></div>`;
  return listHtml;
}
