import "dotenv/config";
import express from "express";
import multer from "multer";
import path from "path";
import { fileURLToPath } from "url";
import crypto from "crypto";
import fs from "fs";
import { Readable } from "stream";
import { buildExecutePayload } from "./lib/weavy-payload.js";
import { browserWeavyFetch } from "./lib/weavy-client-playwright.js";
import * as Accounts from "./lib/account-store.js";
import * as Vouchers from "./lib/voucher-store.js";
import { pollBatchStatus } from "./lib/weavy-poll.js";
import {
  triggerAutoToken,
  startCreatorScheduler,
  stopCreatorScheduler,
  startReviveScheduler,
  stopReviveScheduler,
  scheduleTestRun,
  getSchedulerStatus,
  addSseClient,
  stopSchedulerBot,
  broadcastLog,
} from "./lib/auto-scheduler.js";
import { refreshExistingAccountToken, ensureKling26Recipe, runBulkFlow, signUpFigmaAndLoginWeavy } from "./lib/auto-token.js";
import * as OwnerSettings from "./lib/owner-settings-store.js";
import * as Finance from "./lib/finance-store.js";
import * as AccessLog from "./lib/access-log-store.js";
import { loadIcloudSettings, saveIcloudSettings } from "./lib/icloud-settings.js";
import { createIcloudRouter } from "./lib/icloud-local.js";
import * as IcloudLocal from "./lib/icloud-local.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
const PORT = process.env.PORT || 3000;
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "admin123";

const WEAVY_API = "https://api.weavy.ai";
const DEFAULT_RECIPE_ID = process.env.WEAVY_RECIPE_ID || "YDyaydo2aT1abHpDqPPy8a";

// Helper: baca ICLOUD config dari settings JSON, fallback .env
function getIcloudConfig() {
  const s = loadIcloudSettings();
  return {
    apiUrl: s.apiUrl || process.env.ICLOUD_API_URL || "https://icloud.studiolabs.web.id",
    sessionToken: s.sessionToken || process.env.ICLOUD_SESSION_TOKEN || "",
    deviceId: s.deviceId || process.env.ICLOUD_DEVICE_ID || "",
    accountId: s.accountId || process.env.ICLOUD_ACCOUNT_ID || "",
  };
}

// Helper: upload hasil video ke tmpfiles.org (1 jam retensi)
// Fallback ke originalUrl jika upload gagal
async function uploadToTmpfiles(originalUrl) {
  const TMPFILES_API = "https://tmpfiles.org/api/v1/upload";
  try {
    console.log(`[Tmpfiles] Downloading from: ${originalUrl.substring(0, 60)}...`);
    const dlResp = await fetch(originalUrl);
    if (!dlResp.ok) {
      console.error(`[Tmpfiles] Download failed: ${dlResp.status}`);
      return originalUrl;
    }
    const buffer = Buffer.from(await dlResp.arrayBuffer());
    console.log(`[Tmpfiles] Downloaded ${(buffer.length / 1024 / 1024).toFixed(1)} MB`);

    // Upload ke tmpfiles.org — manual multipart form (lebih reliable dari FormData+Blob di Node.js)
    const boundary = "----TmpfilesBoundary" + Math.random().toString(36).substring(2, 15);
    const crlf = "\r\n";
    const header = [
      `--${boundary}`,
      `Content-Disposition: form-data; name="file"; filename="kling-result.mp4"`,
      `Content-Type: video/mp4`,
      ``,
    ].join(crlf);
    const expirePart = [
      `--${boundary}`,
      `Content-Disposition: form-data; name="expire"`,
      ``,
      `3600`,
    ].join(crlf);
    const footer = `--${boundary}--`;
    const bodyBuffer = Buffer.concat([
      Buffer.from(header + crlf, "utf-8"),
      buffer,
      Buffer.from(crlf + expirePart + crlf + footer, "utf-8"),
    ]);

    const upResp = await fetch(TMPFILES_API, {
      method: "POST",
      headers: { "Content-Type": `multipart/form-data; boundary=${boundary}` },
      body: bodyBuffer,
    });
    const upText = await upResp.text();
    let upData;
    try { upData = JSON.parse(upText); } catch { upData = { status: "error", raw: upText.substring(0, 300) }; }

    if (upData?.status === "success" && upData?.data?.url) {
      let tmpUrl = upData.data.url.replace(/^https?:\/\//, "https://");
      // tmpfiles.org API returns HTML page URL: https://tmpfiles.org/{id}/file
      // Direct download needs /dl/ path: https://tmpfiles.org/dl/{id}/file
      tmpUrl = tmpUrl.replace(/^https:\/\/tmpfiles\.org\/(?!dl\/)(.+)$/, "https://tmpfiles.org/dl/$1");
      console.log(`[Tmpfiles] Upload sukses: ${tmpUrl}`);
      return tmpUrl;
    }
    console.error("[Tmpfiles] Upload gagal:", JSON.stringify(upData).substring(0, 300));
    return originalUrl; // fallback
  } catch (err) {
    console.error(`[Tmpfiles] Error: ${err.message}`);
    return originalUrl; // fallback
  }
}

const activeJobs = new Map();
const activeAssets = new Map();

const JOBS_FILE = path.join(__dirname, "data/active_jobs.json");
const ASSETS_FILE = path.join(__dirname, "data/active_assets.json");

function saveJobsToDisk() {
  try {
    const data = {};
    for (const [k, v] of activeJobs.entries()) {
      data[k] = v;
    }
    const dir = path.dirname(JOBS_FILE);
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }
    fs.writeFileSync(JOBS_FILE, JSON.stringify(data, null, 2), "utf8");
  } catch (err) {
    console.error("Failed to save active jobs:", err);
  }
}

function saveAssetsToDisk() {
  try {
    const data = {};
    for (const [k, v] of activeAssets.entries()) {
      data[k] = v;
    }
    const dir = path.dirname(ASSETS_FILE);
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }
    fs.writeFileSync(ASSETS_FILE, JSON.stringify(data, null, 2), "utf8");
  } catch (err) {
    console.error("Failed to save active assets:", err);
  }
}

async function startServerSidePolling(jobId) {
  console.log(`[BgPoll] Starting background polling for ${jobId}...`);
  const maxAttempts = 180; // Up to 15 minutes (180 attempts * 5 seconds = 900 seconds)
  const estimatedMaxTimeSec = 85;

  for (let i = 0; i < maxAttempts; i++) {
    const job = activeJobs.get(jobId);
    if (!job) {
      console.log(`[BgPoll] Job ${jobId} not found, stopping background poll.`);
      return;
    }

    // Stop polling if completed or failed
    if (job.status === "COMPLETED" || job.status === "FAILED") {
      return;
    }

    const acc = Accounts.getAccount(job.accountId);
    if (!acc) {
      job.status = "FAILED";
      job.error = "Akun pengirim tidak ditemukan di pool.";
      refundJobAccountRun(job);
      saveJobsToDisk();
      return;
    }

    try {
      const result = await pollBatchStatus({ token: acc.token, recipeId: job.recipeId, batchId: job.batchId });

      if (!result.ok) {
        if (result.status === 401 && acc.email && acc.password) {
          console.log(`[BgPoll] Got 401 during polling for ${jobId}. Attempting auto-revive for ${acc.email}...`);
          try {
            const reviveResult = await refreshExistingAccountToken(acc.id, acc.email, acc.password).catch(() => null);
            if (reviveResult && reviveResult.ok) {
              console.log(`[BgPoll] Auto-revive succeeded for ${acc.email}! Retrying...`);
              continue;
            }
          } catch (e) {
            console.error(`[BgPoll] Auto-revive failed for ${acc.email}:`, e);
          }
        }

        console.warn(`[BgPoll] Error polling ${jobId} (Attempt ${i + 1}/${maxAttempts}):`, result.error || `HTTP ${result.status}`);
        // If Weavy returns 404 persistently, wait up to 6 times (30 seconds) before failing
        if (result.status === 404 && i > 5) {
          job.status = "FAILED";
          job.error = "Proses generate tidak ditemukan di server Weavy.";
          refundJobAccountRun(job);
          saveJobsToDisk();
          return;
        }
        await new Promise(r => setTimeout(r, 5000));
        continue;
      }

      const elapsedSec = (i + 1) * 5;
      const percent = Math.min(98, 15 + Math.floor((elapsedSec / estimatedMaxTimeSec) * 80));

      const status = result.status || "RUNNING";
      const error = result.error || null;
      const url = result.url || null;

      if (status === "FAILED" || status === "CANCELLED") {
        // Cek credit issue → auto-retry dengan akun pool lain
        const errText = String(error || "").toLowerCase();
        const isCreditIssue = /credit|insufficient|balance|billing|insufficient credits/i.test(errText);
        if (isCreditIssue && job.retryPayload && !job._retryAttempted) {
          job._retryAttempted = true;
          saveJobsToDisk();
          console.log(`[BgPoll] Credit issue on job ${jobId}, attempting auto-retry with different account...`);

          // Refund run counter untuk akun lama
          const oldAcc = Accounts.getAccount(job.accountId);
          if (oldAcc && (oldAcc.runs || 0) > 0) {
            Accounts.updateAccount(job.accountId, { runs: oldAcc.runs - 1 });
          }

          // Cari akun pool lain
          const newAcc = await getOrReviveAccount(new Set([job.accountId]));
          if (newAcc) {
            const p = job.retryPayload;
            const payload = buildExecutePayload({
              imageUrl: p.imageUrl, imageMeta: p.imageMeta,
              videoUrl: p.videoUrl, videoMeta: p.videoMeta,
              prompt: p.prompt, modelType: p.modelType, version: p.version,
              keepOriginalSound: p.keepOriginalSound, characterOrientation: p.characterOrientation,
              numberOfRuns: p.numberOfRuns, recipeNodes: p.recipeNodes,
            });

            const retryR = await weavyFetch(`/api/v1/batches/recipes/${newAcc.recipeId || DEFAULT_RECIPE_ID}/execute`, {
              method: "POST", token: newAcc.token, body: payload,
              extraHeaders: { "x-app-recipeid": newAcc.recipeId || DEFAULT_RECIPE_ID },
            });
            const retryData = await readResponse(retryR);
            if (retryR.ok) {
              Accounts.markUsed(newAcc.id);
              const newBatchId = retryData.json?.batchId || retryData.json?.id || "";
              job.accountId = newAcc.id;
              job.batchId = newBatchId;
              job.recipeId = newAcc.recipeId || DEFAULT_RECIPE_ID;
              job.status = "PENDING";
              job.error = null;
              job._retryAttempted = false;
              saveJobsToDisk();
              console.log(`[BgPoll] Auto-retry SUCCESS: switched to ${newAcc.email || newAcc.label}, new batch ${newBatchId}`);
              broadcastLog({ type: "info", msg: `🔄 [Auto-Retry] Akun ${oldAcc?.email || oldAcc?.label || "lama"} kehabisan kredit, generate dirotasi ke ${newAcc.email || newAcc.label}.` });
              return; // re-enter polling loop
            }
            console.warn(`[BgPoll] Auto-retry failed: ${retryR.status}`, retryData.json || retryData.text);
          }
          console.warn(`[BgPoll] Auto-retry: no alternative account available for job ${jobId}`);
        }

        job.status = "FAILED";
        job.error = error || `Task ${status.toLowerCase()} di Weavy.`;
        refundJobAccountRun(job);
        saveJobsToDisk();
        return;
      }

      if (status === "COMPLETED" && url) {
        // Upload ke tmpfiles.org biar durasi 1 jam sinkron (tidak tergantung URL Weavy)
        console.log(`[BgPoll] Job ${jobId} COMPLETED — uploading to tmpfiles.org...`);
        const stableUrl = await uploadToTmpfiles(url);

        const assetId = "asset_res_" + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
        activeAssets.set(assetId, {
          assetId,
          realUrl: stableUrl,
          realThumbnailUrl: "",
          metadata: { originalUrl: url },
          token: acc.token,
          createdAt: Date.now()
        });
        saveAssetsToDisk();

        job.status = "COMPLETED";
        job.progress = 100;
        job.url = `/api/assets/view/${assetId}`;
        job.originalUrl = url; // backup original Weavy URL for asset recovery
        job.duration = result.duration || null;

        try {
          Accounts.markUsed(job.accountId);
          console.log(`[BgPoll] Account ${job.accountId} marked as used after successful completion of job ${jobId}`);
        } catch (accErr) {
          console.error(`[BgPoll] Failed to mark account ${job.accountId} as used:`, accErr.message);
        }

        if (job.voucherCode) {
          try {
            Vouchers.incrementUsage(job.voucherCode, job.batchId);
          } catch (vchErr) {
            console.error("[BgPoll] Failed to increment usage:", vchErr.message);
          }
        }
        saveJobsToDisk();
        return;
      }

      // Update progress
      job.status = status;
      job.progress = percent;
      job.updatedAt = Date.now();
      saveJobsToDisk();

    } catch (pollErr) {
      console.error(`[BgPoll] Exception in bg poll for ${jobId}:`, pollErr);
    }

    await new Promise(r => setTimeout(r, 5000));
  }

  // Timeout after 15 minutes
  const job = activeJobs.get(jobId);
  if (job && job.status !== "COMPLETED" && job.status !== "FAILED") {
    job.status = "FAILED";
    job.error = "Polling timeout di server (15 menit).";
    refundJobAccountRun(job);
    saveJobsToDisk();
    cancelWeavyBatch(jobId).catch((err) => {
      console.warn(`[TimeoutCancel] Gagal membatalkan batch Weavy untuk ${jobId}: ${err.message}`);
    });
  }
}

function loadJobsFromDisk() {
  try {
    if (fs.existsSync(JOBS_FILE)) {
      const content = fs.readFileSync(JOBS_FILE, "utf8");
      const data = JSON.parse(content);
      const now = Date.now();
      let resumed = 0;
      for (const k of Object.keys(data)) {
        const job = data[k];
        // Load jobs within last 24 hours
        if (now - job.createdAt < 24 * 60 * 60 * 1000) {
          activeJobs.set(k, job);
          if (job.status !== "COMPLETED" && job.status !== "FAILED") {
            startServerSidePolling(k);
            resumed++;
          }
        }
      }
      console.log(`[BgPoll] Loaded ${activeJobs.size} jobs from disk, resumed background polling for ${resumed} running jobs.`);
    }
  } catch (err) {
    console.error("Failed to load active jobs:", err);
  }
}

function loadAssetsFromDisk() {
  try {
    if (fs.existsSync(ASSETS_FILE)) {
      const content = fs.readFileSync(ASSETS_FILE, "utf8");
      const data = JSON.parse(content);
      const now = Date.now();
      for (const k of Object.keys(data)) {
        const asset = data[k];
        // Load assets within last 24 hours
        if (now - asset.createdAt < 24 * 60 * 60 * 1000) {
          activeAssets.set(k, asset);
        }
      }
      console.log(`[Assets] Loaded ${activeAssets.size} assets from disk.`);
    }
  } catch (err) {
    console.error("Failed to load active assets:", err);
  }
}

function refundJobAccountRun(job) {
  if (!job || job.refunded) return;
  const acc = Accounts.getAccount(job.accountId);
  if (acc && (acc.runs || 0) > 0) {
    Accounts.updateAccount(job.accountId, { runs: acc.runs - 1 });
    console.log(`[Refund] Mengembalikan 1 runs untuk akun: ${acc.email || acc.label}. Runs sekarang: ${acc.runs - 1}`);
  }
  job.refunded = true;
}

function addJob(accountId, batchId, recipeId, voucherCode, retryPayload = null, customJobId = null) {
  const jobId = customJobId || ("job_" + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15));
  activeJobs.set(jobId, {
    accountId,
    batchId,
    recipeId,
    voucherCode,
    retryPayload,
    status: "PENDING",
    progress: 5,
    url: null,
    duration: null,
    error: null,
    refunded: false,
    createdAt: Date.now(),
    updatedAt: Date.now()
  });
  saveJobsToDisk();
  startServerSidePolling(jobId);
  return jobId;
}

function registerAndMaskAsset(asset, token) {
  if (!asset) return { maskedAsset: asset, assetId: "" };

  const assetId = "asset_" + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
  
  const realUrl = asset.url || asset.secureUrl || asset.secure_url || "";
  const realThumbnailUrl = asset.thumbnailUrl || "";

  activeAssets.set(assetId, {
    assetId,
    realUrl,
    realThumbnailUrl,
    metadata: {
      width: asset.width,
      height: asset.height,
      duration: asset.duration,
      fps: asset.fps,
      hasAudio: asset.hasAudio,
      publicId: asset.publicId || asset.public_id,
      id: asset.id,
      name: asset.name
    },
    token,
    createdAt: Date.now()
  });

  const maskedAsset = { ...asset };
  if (maskedAsset.url) maskedAsset.url = `/api/assets/view/${assetId}`;
  if (maskedAsset.secureUrl) maskedAsset.secureUrl = `/api/assets/view/${assetId}`;
  if (maskedAsset.secure_url) maskedAsset.secure_url = `/api/assets/view/${assetId}`;
  if (maskedAsset.thumbnailUrl) maskedAsset.thumbnailUrl = `/api/assets/view/${assetId}?thumbnail=true`;

  if (maskedAsset.id) maskedAsset.id = `masked_id_${assetId}`;
  if (maskedAsset.publicId) maskedAsset.publicId = `masked_pub_${assetId}`;
  if (maskedAsset.public_id) maskedAsset.public_id = `masked_pub_${assetId}`;

  saveAssetsToDisk();

  return { maskedAsset, assetId };
}

function registerAndMaskUploadResponse(data, token) {
  if (!data) return data;
  let target = null;
  let key = null;

  if (data.asset) {
    target = data.asset;
    key = "asset";
  } else if (data.file) {
    target = data.file;
    key = "file";
  } else {
    target = data;
  }

  const { maskedAsset } = registerAndMaskAsset(target, token);
  
  const response = { ...data };
  if (key) {
    response[key] = maskedAsset;
  } else {
    Object.assign(response, maskedAsset);
  }
  
  return response;
}

function restoreAssetDetail(url, meta) {
  if (typeof url !== "string") return { url, meta };

  const match = url.match(/asset_[a-z0-9]+/);
  if (match) {
    const assetId = match[0];
    const asset = activeAssets.get(assetId);
    if (asset) {
      let realUrl = asset.realUrl;
      if (url.includes("thumbnail=true")) {
        realUrl = asset.realThumbnailUrl;
      }
      
      const restoredMeta = { ...meta };
      if (asset.metadata) {
        if (restoredMeta.id && asset.metadata.id) restoredMeta.id = asset.metadata.id;
        if (restoredMeta.publicId && asset.metadata.publicId) restoredMeta.publicId = asset.metadata.publicId;
        if (restoredMeta.public_id && asset.metadata.publicId) restoredMeta.public_id = asset.metadata.publicId;
      }

      if (restoredMeta.thumbnailUrl && typeof restoredMeta.thumbnailUrl === "string") {
        const thumbMatch = restoredMeta.thumbnailUrl.match(/asset_[a-z0-9]+/);
        if (thumbMatch) {
          const thumbAsset = activeAssets.get(thumbMatch[0]);
          if (thumbAsset) {
            restoredMeta.thumbnailUrl = thumbAsset.realThumbnailUrl;
          }
        }
      }

      return { url: realUrl, meta: restoredMeta };
    }
  }
  return { url, meta };
}

setInterval(() => {
  const now = Date.now();
  let deletedJobs = false;
  for (const [jobId, job] of activeJobs.entries()) {
    if (now - job.createdAt > 24 * 60 * 60 * 1000) { // 24 hours TTL
      activeJobs.delete(jobId);
      deletedJobs = true;
    }
  }
  if (deletedJobs) {
    saveJobsToDisk();
  }
  let deletedAssets = false;
  for (const [assetId, asset] of activeAssets.entries()) {
    if (now - asset.createdAt > 24 * 60 * 60 * 1000) { // 24 hours TTL
      activeAssets.delete(assetId);
      deletedAssets = true;
    }
  }
  if (deletedAssets) {
    saveAssetsToDisk();
  }
}, 10 * 60 * 1000);

const CORS_ALLOWED = process.env.CORS_ALLOWED_ORIGINS
  ? process.env.CORS_ALLOWED_ORIGINS.split(",").map((s) => s.trim())
  : ["http://localhost:3000", "http://localhost:3001"];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  const isExtension = origin && (
    origin.startsWith("chrome-extension://") ||
    origin.startsWith("moz-extension://") ||
    origin.startsWith("safari-web-extension://") ||
    origin.startsWith("extension://")
  );

  if (origin && (isExtension || CORS_ALLOWED.includes(origin))) {
    res.setHeader("Access-Control-Allow-Origin", origin);
  } else if (!origin) {
    // Allow non-browser requests (e.g. curl, server-to-server)
    res.setHeader("Access-Control-Allow-Origin", "*");
  } else {
    // Origin not allowed — respond with the first allowed origin or empty
    res.setHeader("Access-Control-Allow-Origin", CORS_ALLOWED[0] || "*");
  }
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-admin-password, x-voucher-code");

  // Disable caching for admin and static JS/HTML/CSS to avoid browser cache issues
  const lowPath = req.path.toLowerCase();
  if (lowPath.startsWith("/admin") || lowPath.endsWith(".html") || lowPath.endsWith(".js") || lowPath.endsWith(".css")) {
    res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
    res.setHeader("Pragma", "no-cache");
    res.setHeader("Expires", "0");
    res.setHeader("Surrogate-Control", "no-store");
  }

  if (req.method === "OPTIONS") {
    return res.sendStatus(200);
  }
  next();
});

app.use(express.json({ limit: "10mb" }));

function maskSensitiveData(data, role) {
  if (role === "owner") {
    return data;
  }
  
  if (!data || typeof data !== "object") {
    if (typeof data === "string") {
      return data
        .replace(/https?:\/\/(?:www\.)?figma\.com/gi, "[masked]")
        .replace(/https?:\/\/weave\.figma\.com/gi, "[masked]")
        .replace(/figma\.com/gi, "[masked]")
        .replace(/weave\.figma\.com/gi, "[masked]")
        .replace(/figma/gi, "System")
        .replace(/weave/gi, "Engine");
    }
    return data;
  }

  // Deep clone to avoid mutating original objects
  const cloned = JSON.parse(JSON.stringify(data));

  const maskObject = (obj) => {
    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        // Strip sensitive internal fields completely
        if (["_accountId", "_accountLabel", "_recipeId", "accountId", "accountUsed", "triedAccounts", "triedIds", "recipeId", "raw"].includes(key)) {
          delete obj[key];
          continue;
        }

        if (typeof obj[key] === "string") {
          obj[key] = obj[key]
            .replace(/https?:\/\/(?:www\.)?figma\.com/gi, "[masked]")
            .replace(/https?:\/\/weave\.figma\.com/gi, "[masked]")
            .replace(/figma\.com/gi, "[masked]")
            .replace(/weave\.figma\.com/gi, "[masked]")
            .replace(/figma/gi, "System")
            .replace(/weave/gi, "Engine");
        } else if (typeof obj[key] === "object" && obj[key] !== null) {
          maskObject(obj[key]);
        }
      }
    }
  };

  maskObject(cloned);
  return cloned;
}

app.use((req, res, next) => {
  const originalJson = res.json;
  res.json = function (body) {
    const role = req.adminRole || "member";
    const maskedBody = maskSensitiveData(body, role);
    return originalJson.call(this, maskedBody);
  };
  next();
});

app.use(express.static(path.join(__dirname, "public")));

// Friendly route for /admin
app.get("/admin", (_req, res) => {
  res.sendFile(path.join(__dirname, "public", "admin.html"));
});

const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 200 * 1024 * 1024 },
});

async function readResponse(res) {
  const text = await res.text();
  let json = null;
  try { json = text ? JSON.parse(text) : null; } catch {}
  return { status: res.status, ok: res.ok, json, text };
}

function extractWeavyError(payload) {
  if (!payload) return "Unknown Weavy error";
  if (typeof payload === "string") return payload;
  if (payload.json) {
    const j = payload.json;
    if (typeof j === "string") return j;
    if (j.statusCode === 403 && j.internalErrorCode === 1076) {
      return `Model "${j.message}" tidak tersedia di plan akun ini.`;
    }
    if (j.message && j.statusCode) return `[${j.statusCode}] ${j.message}${j.internalErrorCode ? ` (code ${j.internalErrorCode})` : ""}`;
    if (j.message) return j.message;
    if (j.error) return typeof j.error === "string" ? j.error : JSON.stringify(j.error);
    if (j.detail) return typeof j.detail === "string" ? j.detail : JSON.stringify(j.detail);
    return JSON.stringify(j);
  }
  return payload.text || `HTTP ${payload.status}`;
}

async function weavyFetch(p, { method = "GET", token, body, extraHeaders } = {}) {
  const headers = {
    accept: "application/json, text/plain, */*",
    "accept-language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7",
    "content-type": "application/json",
    "x-weavy-auth-provider": "firebase",
    "x-app-version": "4.1.577",
    authorization: `Bearer ${token}`,
    ...extraHeaders,
  };
  return browserWeavyFetch(`${WEAVY_API}${p}`, {
    method,
    headers,
    body: body ? JSON.stringify(body) : undefined,
  });
}

// ─── Member Voucher Auth middleware ──────────────────────────────────────────

function requireVoucher(req, res, next) {
  const code = req.headers["x-voucher-code"];
  if (!code) return res.status(401).json({ error: "Kode voucher diperlukan." });

  const val = Vouchers.validateVoucher(code);
  if (!val.ok) return res.status(401).json({ error: val.error });

  req.voucher = val.voucher;
  next();
}

// In-memory rate limiting map for login and voucher attempts
const loginAttempts = new Map();

function checkRateLimit(ip, limit = 5, windowMs = 5 * 60 * 1000) {
  const now = Date.now();
  if (!loginAttempts.has(ip)) {
    loginAttempts.set(ip, []);
    return { blocked: false };
  }
  const history = loginAttempts.get(ip).filter(t => now - t < windowMs);
  loginAttempts.set(ip, history);
  if (history.length >= limit) {
    const remainingTime = Math.ceil((history[0] + windowMs - now) / 1000);
    return { blocked: true, seconds: remainingTime };
  }
  return { blocked: false };
}

function recordAttempt(ip) {
  if (!loginAttempts.has(ip)) {
    loginAttempts.set(ip, []);
  }
  loginAttempts.get(ip).push(Date.now());
}

function clearAttempts(ip) {
  loginAttempts.delete(ip);
}

app.post("/api/auth/login", (req, res) => {
  const ip = req.ip || req.headers["x-forwarded-for"] || req.socket.remoteAddress;
  const limitCheck = checkRateLimit(ip, 8, 3 * 60 * 1000); // 8 attempts per 3 mins
  if (limitCheck.blocked) {
    return res.status(429).json({ error: `Terlalu banyak percobaan aktivasi. Harap coba lagi dalam ${limitCheck.seconds} detik.` });
  }

  const { voucherCode } = req.body || {};
  const val = Vouchers.validateVoucher(voucherCode);
  if (!val.ok) {
    recordAttempt(ip);
    return res.status(401).json({ error: val.error });
  }
  clearAttempts(ip);

  let promo = null;
  const settings = OwnerSettings.getSettings();
  const creatorId = val.voucher?.createdBy;
  if (creatorId && creatorId.startsWith("mitra_")) {
    const reseller = settings.resellers.find(r => r.id === creatorId);
    if (reseller && reseller.promo && reseller.promo.active) {
      promo = reseller.promo;
    }
  } else {
    if (settings.globalPromo && settings.globalPromo.active) {
      promo = settings.globalPromo;
    }
  }

  res.json({ ok: true, tier: val.tier, promo });
});

app.get("/api/auth/voucher-status", requireVoucher, (req, res) => {
  const todayStr = new Date().toISOString().split("T")[0];
  const usage = req.voucher.usage?.[todayStr] || 0;
  const now = new Date();
  const expiresAtDate = new Date(req.voucher.expiresAt);
  const remainingMs = expiresAtDate.getTime() - now.getTime();
  const remainingDays = Math.max(0, Math.ceil(remainingMs / (1000 * 60 * 60 * 24)));
  res.json({
    ok: true,
    code: req.voucher.code,
    tier: req.voucher.tier,
    dailyLimit: req.voucher.dailyLimit || 3,
    usage: usage,
    expiresAt: req.voucher.expiresAt,
    remainingDays: remainingDays,
  });
});

function requireAdmin(req, res, next) {
  const pass = req.headers["x-admin-password"];
  const loginInfo = OwnerSettings.validateLogin(pass);
  if (!loginInfo.ok) return res.status(401).json({ error: "Unauthorized" });
  req.adminRole = loginInfo.role;
  req.resellerInfo = loginInfo; // Contains resellerId, resellerName, quota if role is "mitra"
  next();
}

function requireOwner(req, res, next) {
  requireAdmin(req, res, () => {
    // Owner selalu lolos
    if (req.adminRole === "owner") return next();
    // Mitra dengan fullAccessGranted juga lolos (setara owner)
    if (req.adminRole === "mitra" && req.resellerInfo?.fullAccessGranted === true) {
      req.isFullAccessMitra = true; // flag untuk pembatasan operasi sensitif
      return next();
    }
    return res.status(403).json({ error: "Akses Ditolak: Hanya Owner atau Mitra dengan akses penuh yang diizinkan." });
  });
}

// Require TRUE owner only — mitra full-access TIDAK bisa lewat
// Digunakan untuk operasi sensitif: ubah password owner, kelola full-access mitra lain
function requireTrueOwner(req, res, next) {
  requireAdmin(req, res, () => {
    if (req.adminRole !== "owner") {
      return res.status(403).json({ error: "Akses Ditolak: Hanya Pemilik Akun Utama (Owner) yang dapat melakukan operasi ini." });
    }
    next();
  });
}

function requireBotAccess(req, res, next) {
  requireAdmin(req, res, () => {
    if (req.adminRole === "mitra" && !req.resellerInfo.allowBot) {
      return res.status(403).json({ error: "Akses Ditolak: Anda tidak memiliki izin untuk mengelola bot." });
    }
    next();
  });
}

function requireArchiveAccess(req, res, next) {
  requireAdmin(req, res, () => {
    if (req.adminRole === "mitra" && !req.resellerInfo.allowArchive) {
      return res.status(403).json({ error: "Akses Ditolak: Anda tidak memiliki izin untuk mengakses arsip." });
    }
    next();
  });
}

// ─── Admin auth ─────────────────────────────────────────────────────────────

app.post("/api/admin/login", (req, res) => {
  const ip = req.ip || req.headers["x-forwarded-for"] || req.socket.remoteAddress;
  const limitCheck = checkRateLimit(ip, 5, 5 * 60 * 1000); // 5 attempts per 5 mins
  if (limitCheck.blocked) {
    return res.status(429).json({ error: `Terlalu banyak percobaan login. Silakan coba lagi dalam ${limitCheck.seconds} detik.` });
  }

  const { password } = req.body || {};
  const loginInfo = OwnerSettings.validateLogin(password);
  if (!loginInfo.ok) {
    recordAttempt(ip);
    return res.status(401).json({ error: "Password admin salah!" });
  }
  clearAttempts(ip);
  const settings = OwnerSettings.getSettings();
  res.json({
    ok: true,
    role: loginInfo.role,
    resellerId: loginInfo.resellerId || null,
    resellerName: loginInfo.resellerName || null,
    quota: loginInfo.quota !== undefined ? loginInfo.quota : null,
    balance: loginInfo.balance !== undefined ? loginInfo.balance : null,
    totalCommission: loginInfo.totalCommission !== undefined ? loginInfo.totalCommission : null,
    customResellerPrice: loginInfo.customResellerPrice !== undefined ? loginInfo.customResellerPrice : null,
    customSrp: loginInfo.customSrp !== undefined ? loginInfo.customSrp : null,
    customResellerVipPrice: loginInfo.customResellerVipPrice !== undefined ? loginInfo.customResellerVipPrice : null,
    customVipSrp: loginInfo.customVipSrp !== undefined ? loginInfo.customVipSrp : null,
    allowBot: loginInfo.allowBot !== undefined ? loginInfo.allowBot : null,
    allowPool: loginInfo.allowPool !== undefined ? loginInfo.allowPool : null,
    allowArchive: loginInfo.allowArchive !== undefined ? loginInfo.allowArchive : null,
    debtEnabled: loginInfo.debtEnabled !== undefined ? loginInfo.debtEnabled : null,
    debtLimit: loginInfo.debtLimit !== undefined ? loginInfo.debtLimit : null,
    fullAccessRequested: loginInfo.fullAccessRequested !== undefined ? loginInfo.fullAccessRequested : null,
    fullAccessGranted: loginInfo.fullAccessGranted !== undefined ? loginInfo.fullAccessGranted : null,
    fullAccessGrantedAt: loginInfo.fullAccessGrantedAt || null,
    globalProPrice: settings.prices?.proPrice || 300000,
    globalVipPrice: settings.prices?.vipPrice || 500000
  });
});

// ─── Owner settings & Reseller CRUD ──────────────────────────────────────────

app.get("/api/admin/owner/settings", requireOwner, (_req, res) => {
  try {
    const settings = OwnerSettings.getSettings();
    res.json(settings);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.post("/api/admin/owner/settings", requireOwner, (req, res) => {
  try {
    const { kasirPassword, prices } = req.body || {};
    const settings = OwnerSettings.getSettings();
    if (kasirPassword !== undefined) {
      const pw = String(kasirPassword).trim();
      if (!pw) return res.status(400).json({ error: "Password kasir tidak boleh kosong" });
      if (pw === "4rg0v14N") return res.status(400).json({ error: "Password kasir tidak boleh sama dengan Owner" });
      settings.kasirPassword = pw;
    }
    if (prices !== undefined) {
      if (prices.proPrice !== undefined) settings.prices.proPrice = parseFloat(prices.proPrice) || 0;
      if (prices.resellerPrice !== undefined) settings.prices.resellerPrice = parseFloat(prices.resellerPrice) || 0;
      if (prices.vipPrice !== undefined) settings.prices.vipPrice = parseFloat(prices.vipPrice) || 0;
      if (prices.resellerVipPrice !== undefined) settings.prices.resellerVipPrice = parseFloat(prices.resellerVipPrice) || 0;
    }
    OwnerSettings.writeSettings(settings);
    res.json({ ok: true, settings });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.post("/api/admin/owner/resellers", requireOwner, (req, res) => {
  try {
    const { name, password, quota, balance, customResellerPrice, customSrp, customResellerVipPrice, customVipSrp, allowBot, allowPool, allowArchive, debtEnabled, debtLimit } = req.body || {};
    const reseller = OwnerSettings.addReseller({ name, password, quota, balance, customResellerPrice, customSrp, customResellerVipPrice, customVipSrp, allowBot, allowPool, allowArchive, debtEnabled, debtLimit });
    res.json({ ok: true, reseller });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

app.put("/api/admin/owner/resellers/:id", requireOwner, (req, res) => {
  try {
    const reseller = OwnerSettings.updateReseller(req.params.id, req.body || {});
    res.json({ ok: true, reseller });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

app.delete("/api/admin/owner/resellers/:id", requireOwner, (req, res) => {
  try {
    const ok = OwnerSettings.deleteReseller(req.params.id);
    if (!ok) return res.status(404).json({ error: "Reseller tidak ditemukan" });
    res.json({ ok: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// ═══ Full Access Management — Mitra request + Owner approve/revoke ═══

// Mitra mengajukan permintaan akses penuh
app.post("/api/admin/reseller/request-full-access", requireAdmin, (req, res) => {
  if (req.adminRole !== "mitra") {
    return res.status(403).json({ error: "Akses Ditolak: Hanya Mitra Reseller yang dapat mengajukan permintaan ini." });
  }
  try {
    const resellerId = req.resellerInfo.resellerId;
    const result = OwnerSettings.requestFullAccess(resellerId);
    // Log aktivitas
    AccessLog.logAccessAction({
      type: "request",
      actor: resellerId,
      actorName: req.resellerInfo.resellerName || "Mitra",
      action: "Mengajukan permintaan akses penuh",
      targetResellerId: resellerId,
      targetResellerName: result.name,
      description: `Mitra "${result.name}" mengajukan permintaan akses penuh (owner-level).`,
      result: "success",
      ip: req.ip || req.headers["x-forwarded-for"] || "",
    });
    res.json({ ok: true, ...result });
  } catch (err) {
    AccessLog.logAccessAction({
      type: "request",
      actor: req.resellerInfo.resellerId,
      actorName: req.resellerInfo.resellerName || "Mitra",
      action: "Gagal mengajukan permintaan akses penuh",
      targetResellerId: req.resellerInfo.resellerId,
      targetResellerName: req.resellerInfo.resellerName || "",
      description: err.message,
      result: "failed",
      ip: req.ip || "",
    });
    res.status(400).json({ error: err.message });
  }
});

// Owner menyetujui akses penuh mitra
app.post("/api/admin/owner/resellers/:id/grant-full-access", requireTrueOwner, (req, res) => {
  try {
    const result = OwnerSettings.grantFullAccess(req.params.id, "owner");
    AccessLog.logAccessAction({
      type: "grant",
      actor: "owner",
      actorName: "Owner",
      action: "Menyetujui akses penuh",
      targetResellerId: req.params.id,
      targetResellerName: result.name,
      description: `Owner menyetujui akses penuh untuk mitra "${result.name}".`,
      result: "success",
      ip: req.ip || "",
    });
    res.json({ ok: true, ...result });
  } catch (err) {
    AccessLog.logAccessAction({
      type: "grant",
      actor: "owner",
      actorName: "Owner",
      action: "Gagal menyetujui akses penuh",
      targetResellerId: req.params.id,
      targetResellerName: "",
      description: err.message,
      result: "failed",
      ip: req.ip || "",
    });
    res.status(400).json({ error: err.message });
  }
});

// Owner mencabut akses penuh mitra
app.post("/api/admin/owner/resellers/:id/revoke-full-access", requireTrueOwner, (req, res) => {
  try {
    const result = OwnerSettings.revokeFullAccess(req.params.id, "owner");
    AccessLog.logAccessAction({
      type: "revoke",
      actor: "owner",
      actorName: "Owner",
      action: "Mencabut akses penuh",
      targetResellerId: req.params.id,
      targetResellerName: result.name,
      description: `Owner mencabut akses penuh dari mitra "${result.name}".`,
      result: "success",
      ip: req.ip || "",
    });
    res.json({ ok: true, ...result });
  } catch (err) {
    AccessLog.logAccessAction({
      type: "revoke",
      actor: "owner",
      actorName: "Owner",
      action: "Gagal mencabut akses penuh",
      targetResellerId: req.params.id,
      targetResellerName: "",
      description: err.message,
      result: "failed",
      ip: req.ip || "",
    });
    res.status(400).json({ error: err.message });
  }
});

// Owner menolak permintaan akses penuh mitra
app.post("/api/admin/owner/resellers/:id/deny-full-access", requireTrueOwner, (req, res) => {
  try {
    const result = OwnerSettings.denyFullAccess(req.params.id);
    AccessLog.logAccessAction({
      type: "deny",
      actor: "owner",
      actorName: "Owner",
      action: "Menolak permintaan akses penuh",
      targetResellerId: req.params.id,
      targetResellerName: result.name,
      description: `Owner menolak permintaan akses penuh dari mitra "${result.name}".`,
      result: "success",
      ip: req.ip || "",
    });
    res.json({ ok: true, ...result });
  } catch (err) {
    AccessLog.logAccessAction({
      type: "deny",
      actor: "owner",
      actorName: "Owner",
      action: "Gagal menolak permintaan akses penuh",
      targetResellerId: req.params.id,
      targetResellerName: "",
      description: err.message,
      result: "failed",
      ip: req.ip || "",
    });
    res.status(400).json({ error: err.message });
  }
});

// Owner melihat log aktivitas akses penuh
app.get("/api/admin/owner/full-access/logs", requireTrueOwner, (req, res) => {
  try {
    const filters = {};
    if (req.query.type) filters.type = req.query.type;
    if (req.query.reseller) filters.targetResellerId = req.query.reseller;
    if (req.query.search) filters.search = req.query.search;
    if (req.query.limit) filters.limit = parseInt(req.query.limit) || 200;
    if (req.query.offset) filters.offset = parseInt(req.query.offset) || 0;
    const result = AccessLog.getAccessLogs(filters);
    res.json({ ok: true, ...result });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Owner melihat ringkasan akses penuh
app.get("/api/admin/owner/full-access/summary", requireTrueOwner, (req, res) => {
  try {
    const summary = AccessLog.getAccessSummary();
    // Tambah data real-time dari owner-settings
    const settings = OwnerSettings.getSettings();
    summary.activeFullAccess = settings.resellers
      .filter((r) => r.fullAccessGranted)
      .map((r) => ({
        id: r.id,
        name: r.name,
        grantedAt: r.fullAccessGrantedAt,
        grantedBy: r.fullAccessGrantedBy,
      }));
    summary.pendingRequests = settings.resellers
      .filter((r) => r.fullAccessRequested && !r.fullAccessGranted)
      .map((r) => ({
        id: r.id,
        name: r.name,
        requestedAt: r.fullAccessRequestedAt,
      }));
    res.json({ ok: true, ...summary });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Log aktivitas akses yang dilakukan oleh mitra full-access (untuk audit)
app.post("/api/admin/audit/access-log", requireOwner, (req, res) => {
  try {
    const { action, targetResellerId, targetResellerName, description } = req.body || {};
    const actor = req.adminRole === "owner" ? "owner" : (req.resellerInfo?.resellerId || "unknown");
    const actorName = req.adminRole === "owner" ? "Owner" : (req.resellerInfo?.resellerName || "Mitra");
    AccessLog.logAccessAction({
      type: "access",
      actor,
      actorName,
      action: action || "Aktivitas akses",
      targetResellerId: targetResellerId || null,
      targetResellerName: targetResellerName || "",
      description: description || "",
      result: "success",
      ip: req.ip || req.headers["x-forwarded-for"] || "",
    });
    res.json({ ok: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.put("/api/admin/reseller/settings", requireAdmin, (req, res) => {
  if (req.adminRole !== "mitra") {
    return res.status(403).json({ error: "Akses Ditolak: Hanya Mitra Reseller yang dapat mengakses pengaturan ini." });
  }
  try {
    const resellerId = req.resellerInfo.resellerId;
    const { customSrp, customVipSrp } = req.body || {};
    
    const settings = OwnerSettings.getSettings();
    const reseller = settings.resellers.find(r => r.id === resellerId);
    if (!reseller) {
      return res.status(400).json({ error: "Akun reseller tidak ditemukan." });
    }

    const globalPro = settings.prices?.proPrice || 300000;
    const globalVip = settings.prices?.vipPrice || 500000;
    
    if (customSrp !== null && customSrp !== "" && parseFloat(customSrp) < globalPro) {
      return res.status(400).json({ error: `Dilarang menjual Pro di bawah harga SRP Global! (Minimum: Rp ${globalPro.toLocaleString('id-ID')})` });
    }
    if (customVipSrp !== null && customVipSrp !== "" && parseFloat(customVipSrp) < globalVip) {
      return res.status(400).json({ error: `Dilarang menjual VIP di bawah harga SRP Global! (Minimum: Rp ${globalVip.toLocaleString('id-ID')})` });
    }
    
    reseller.customSrp = customSrp !== null && customSrp !== "" ? parseFloat(customSrp) : null;
    reseller.customVipSrp = customVipSrp !== null && customVipSrp !== "" ? parseFloat(customVipSrp) : null;
    
    OwnerSettings.writeSettings(settings);
    res.json({ ok: true, customSrp: reseller.customSrp, customVipSrp: reseller.customVipSrp });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.get("/api/admin/reseller/profile", requireAdmin, (req, res) => {
  if (req.adminRole !== "mitra") {
    return res.status(403).json({ error: "Akses Ditolak: Hanya Mitra Reseller yang dapat mengakses profil ini." });
  }
  try {
    const resellerId = req.resellerInfo.resellerId;
    const settings = OwnerSettings.getSettings();
    const reseller = settings.resellers.find(r => r.id === resellerId);
    if (!reseller) {
      return res.status(400).json({ error: "Akun reseller tidak ditemukan." });
    }
    res.json({
      ok: true,
      balance: reseller.balance ?? 0,
      totalCommission: reseller.totalCommission ?? 0,
      customSrp: reseller.customSrp ?? null,
      customVipSrp: reseller.customVipSrp ?? null,
      globalProPrice: settings.prices?.proPrice || 300000,
      globalVipPrice: settings.prices?.vipPrice || 500000,
      debtEnabled: reseller.debtEnabled ?? false,
      debtLimit: reseller.debtLimit ?? 0,
      isDebt: (reseller.balance || 0) < 0,
      debtAmount: (reseller.balance || 0) < 0 ? Math.abs(reseller.balance) : 0,
    });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.get("/api/admin/accounts", requireAdmin, (req, res) => {
  if (req.adminRole === "mitra" && !req.resellerInfo.allowPool) {
    return res.status(403).json({ error: "Akses Ditolak: Anda tidak diizinkan melihat pool akun." });
  }
  res.json({ accounts: Accounts.listAccountsSafe() });
});

app.get("/api/admin/accounts/archive", requireArchiveAccess, (_req, res) => {
  res.json({ accounts: Accounts.listArchivedAccountsSafe() });
});

app.get("/api/admin/accounts/raw", requireAdmin, (req, res) => {
  if (req.adminRole === "mitra" && !req.resellerInfo?.allowPool) {
    return res.status(403).json({ error: "Akses Ditolak: Anda tidak diizinkan melihat pool akun." });
  }
  res.json({ accounts: Accounts.listAccounts() });
});

app.post("/api/admin/accounts", requireAdmin, (req, res) => {
  try {
    const {
      token,
      recipeId,
      label,
      email,
      password,
      runs,
      maxRuns,
      credits,
      expired,
      archived,
      archivedAt,
      archivedMonth,
      archiveReason,
      restoredAt,
    } = req.body || {};
    if (!token) return res.status(400).json({ error: "token required" });
    const acc = Accounts.addAccount({
      token: token.trim(),
      recipeId: recipeId?.trim() || "",
      label: label?.trim() || "",
      email: email?.trim() || "",
      password: password?.trim() || "",
      runs,
      maxRuns,
      credits,
      expired,
      archived,
      archivedAt,
      archivedMonth,
      archiveReason,
      restoredAt,
    });
    res.json({ account: acc });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

app.put("/api/admin/accounts/:id", requireAdmin, (req, res) => {
  try {
    const acc = Accounts.updateAccount(req.params.id, req.body || {});
    res.json({ account: acc });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

app.post("/api/admin/accounts/:id/restore", requireArchiveAccess, (req, res) => {
  try {
    const { force = false } = req.body || {};
    const acc = Accounts.restoreArchivedAccount(req.params.id, { force });
    res.json({ account: acc });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// Revive akun expired — login ulang via Figma Sign-In untuk refresh token
app.post("/api/admin/accounts/:id/revive", requireAdmin, async (req, res) => {
  const acc = Accounts.getAccount(req.params.id);
  if (!acc) return res.status(404).json({ error: "Akun tidak ditemukan." });
  if (!acc.email) return res.status(400).json({ error: "Akun tidak memiliki email (tidak bisa di-revive)." });

  // Cek apakah akun di-block
  if (acc.figmaBlocked || acc.googleBlocked) {
    const reason = acc.figmaBlockedReason || acc.googleBlockedReason || "tidak diketahui";
    return res.status(400).json({ error: `Akun sudah di-block (${reason}). Tidak bisa di-revive.` });
  }

  broadcastLog({ type: "info", msg: `🔄 [Manual Revive] Memulai revive Figma untuk ${acc.email}...` });

  try {
    const result = await refreshExistingAccountToken(acc.id, acc.email, acc.password || "", (event) => broadcastLog(event));
    if (result.ok) {
      broadcastLog({ type: "success", msg: `✅ [Manual Revive] ${acc.email} berhasil di-revive!` });
      const updated = Accounts.getAccount(acc.id);
      return res.json({ ok: true, account: updated || acc });
    }
    broadcastLog({ type: "error", msg: `❌ [Manual Revive] ${acc.email} gagal: ${result.error}` });
    return res.status(500).json({ error: result.error || "Gagal revive akun." });
  } catch (err) {
    broadcastLog({ type: "error", msg: `❌ [Manual Revive] ${acc.email} error: ${err.message}` });
    return res.status(500).json({ error: err.message });
  }
});

app.delete("/api/admin/accounts/by-email", requireOwner, (req, res) => {
  const { email } = req.query;
  if (!email) return res.status(400).json({ error: "email required" });
  const ok = Accounts.deleteAccountByEmail(email);
  if (!ok) return res.status(404).json({ error: "Not found" });
  res.json({ ok: true });
});

app.delete("/api/admin/accounts/:id", requireOwner, (req, res) => {
  const ok = Accounts.deleteAccount(req.params.id);
  if (!ok) return res.status(404).json({ error: "Not found" });
  res.json({ ok: true });
});

app.post("/api/admin/accounts/delete-multiple", requireOwner, (req, res) => {
  const { ids } = req.body || {};
  if (!Array.isArray(ids)) return res.status(400).json({ error: "ids required" });
  const count = Accounts.deleteAccounts(ids);
  res.json({ ok: true, deleted: count });
});

app.post("/api/admin/accounts/archive-multiple", requireOwner, (req, res) => {
  const { ids } = req.body || {};
  if (!Array.isArray(ids)) return res.status(400).json({ error: "ids required" });
  const count = Accounts.archiveAccounts(ids, { reason: "manual" });
  res.json({ ok: true, archived: count });
});
app.post("/api/admin/accounts/clean", requireOwner, (_req, res) => {
  const count = Accounts.deleteExpiredAccounts();
  res.json({ ok: true, cleaned: count });
});

app.post("/api/admin/accounts/archive/delete-all", requireOwner, (_req, res) => {
  try {
    const count = Accounts.deleteAllArchivedAccounts();
    res.json({ ok: true, deleted: count });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.post("/api/admin/accounts/pool/delete-all", requireOwner, (_req, res) => {
  try {
    const count = Accounts.deleteAllActiveAccounts();
    res.json({ ok: true, deleted: count });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});


// Bulk import: paste many tokens at once (one per line, optionally TAB-separated with recipeId & label)
app.post("/api/admin/accounts/bulk", requireOwner, (req, res) => {
  const { lines } = req.body || {};
  if (!Array.isArray(lines)) return res.status(400).json({ error: "lines: string[] required" });

  const added = [];
  const errors = [];
  for (const line of lines) {
    const trimmed = String(line).trim();
    if (!trimmed) continue;
    const parts = trimmed.split(/\t|,/).map((s) => s.trim());
    const [token, recipeId = "", label = ""] = parts;
    try {
      added.push(Accounts.addAccount({ token, recipeId, label }));
    } catch (e) {
      errors.push({ line: trimmed.slice(0, 40), error: e.message });
    }
  }
  res.json({ ok: true, added: added.length, errors });
});

// ─── Admin API: voucher management ──────────────────────────────────────────

app.get("/api/admin/vouchers", requireAdmin, async (req, res) => {
  let remoteVouchers = [];
  if (process.env.REMOTE_SERVER_URL) {
    const remoteUrl = process.env.REMOTE_SERVER_URL.replace(/\/$/, "");
    try {
      const response = await fetch(`${remoteUrl}/api/admin/vouchers`, {
        method: "GET",
        headers: {
          "x-admin-password": req.headers["x-admin-password"]
        }
      });
      const data = await response.json().catch(() => ({}));
      if (response.ok) {
        remoteVouchers = Array.isArray(data.vouchers) ? data.vouchers : [];
      } else {
        console.warn(`[Proxy GET Vouchers] Remote returned ${response.status}, falling back to local only`);
      }
    } catch (e) {
      console.error(`[Proxy GET Vouchers] Error: ${e.message}, falling back to local only`);
    }
  }

  const localVouchers = Vouchers.listVouchers();
  // Merge: prefer local, supplement with remote (de-dupe by code)
  const merged = [...localVouchers];
  for (const rV of remoteVouchers) {
    if (!merged.some(v => v.code === rV.code)) merged.push(rV);
  }

  if (req.adminRole === "mitra") {
    // Mitra: hanya vouchernya sendiri. Fallback chain:
    // 1. createdBy === resellerId (paling akurat)
    // 2. createdByName === resellerName (fallback jika ID berubah)
    // 3. createdBy === "mitra" (fallback terakhir)
    const myId = req.resellerInfo.resellerId;
    const myName = (req.resellerInfo.resellerName || "").toLowerCase().trim();
    const filtered = merged.filter((v) => {
      if (myId && v.createdBy === myId) return true;
      const vName = (v.createdByName || "").toLowerCase().trim();
      if (myName && vName && vName === myName) return true;
      if (v.createdBy === "mitra" && myName && vName === myName) return true;
      return false;
    });
    return res.json({ vouchers: filtered });
  }
  res.json({ vouchers: merged });
});

app.get("/api/admin/vouchers/expiring", requireAdmin, async (req, res) => {
  const days = parseInt(req.query.days, 10) || 2;
  let remoteVouchers = [];
  if (process.env.REMOTE_SERVER_URL) {
    const remoteUrl = process.env.REMOTE_SERVER_URL.replace(/\/$/, "");
    try {
      const response = await fetch(`${remoteUrl}/api/admin/vouchers`, {
        method: "GET",
        headers: { "x-admin-password": req.headers["x-admin-password"] }
      });
      const data = await response.json().catch(() => ({}));
      if (response.ok) remoteVouchers = Array.isArray(data.vouchers) ? data.vouchers : [];
    } catch (e) { /* ignore */ }
  }

  const localVouchers = Vouchers.listVouchers();
  const merged = [...localVouchers];
  for (const rV of remoteVouchers) {
    if (!merged.some(v => v.code === rV.code)) merged.push(rV);
  }

  const now = Date.now();
  const threshold = days * 24 * 60 * 60 * 1000;
  const expiring = merged.filter(v => {
    if (!v.expiresAt) return false;
    const exp = new Date(v.expiresAt).getTime();
    return exp > now && exp - now <= threshold;
  }).map(v => {
    const exp = new Date(v.expiresAt).getTime();
    return {
      code: v.code,
      tier: v.tier,
      whatsapp: v.whatsapp,
      expiresAt: v.expiresAt,
      daysLeft: Math.max(0, Math.ceil((exp - now) / (24 * 60 * 60 * 1000))),
      createdBy: v.createdBy,
      createdByName: v.createdByName,
    };
  }).sort((a, b) => a.daysLeft - b.daysLeft);

  // Mitra: hanya vouchernya sendiri (fallback chain)
  const scoped = (req.adminRole === "mitra")
    ? expiring.filter(v => {
        const myId = req.resellerInfo.resellerId;
        const myName = (req.resellerInfo.resellerName || "").toLowerCase().trim();
        if (myId && v.createdBy === myId) return true;
        const vName = (v.createdByName || "").toLowerCase().trim();
        if (myName && vName && vName === myName) return true;
        if (v.createdBy === "mitra" && myName && vName === myName) return true;
        return false;
      })
    : expiring;

  res.json({ thresholdDays: days, expiring: scoped, total: scoped.length });
});

app.get("/api/admin/vouchers/stats", requireAdmin, async (req, res) => {
  let remoteVouchers = [];
  if (process.env.REMOTE_SERVER_URL) {
    const remoteUrl = process.env.REMOTE_SERVER_URL.replace(/\/$/, "");
    try {
      const response = await fetch(`${remoteUrl}/api/admin/vouchers`, {
        method: "GET",
        headers: { "x-admin-password": req.headers["x-admin-password"] }
      });
      const data = await response.json().catch(() => ({}));
      if (response.ok) {
        remoteVouchers = Array.isArray(data.vouchers) ? data.vouchers : [];
      }
    } catch (e) { /* ignore */ }
  }

  const localVouchers = Vouchers.listVouchers();
  const merged = [...localVouchers];
  for (const rV of remoteVouchers) {
    if (!merged.some(v => v.code === rV.code)) merged.push(rV);
  }

  // Mitra: hanya boleh lihat statistik dirinya sendiri
  const scopedVouchers = (req.adminRole === "mitra")
    ? merged.filter((v) => v.createdBy === req.resellerInfo.resellerId)
    : merged;

  // Statistik per creator
  const byCreator = {};
  let activeCount = 0, expiredCount = 0, proCount = 0, vipCount = 0;
  const now = Date.now();

  for (const v of scopedVouchers) {
    const key = v.createdBy || "owner";
    if (!byCreator[key]) byCreator[key] = { name: v.createdByName || key, total: 0, active: 0, expired: 0, pro: 0, vip: 0 };
    byCreator[key].total++;
    if (v.tier === "pro") byCreator[key].pro++;
    if (v.tier === "vip") byCreator[key].vip++;
    if (v.tier === "pro") proCount++;
    if (v.tier === "vip") vipCount++;
    if (v.expiresAt && new Date(v.expiresAt).getTime() < now) {
      byCreator[key].expired++;
      expiredCount++;
    } else {
      byCreator[key].active++;
      activeCount++;
    }
  }

  // Hanya owner boleh menerima statistik total seluruh server
  const includeBreakdown = (req.adminRole === "owner");

  return res.json({
    total: scopedVouchers.length,
    active: activeCount,
    expired: expiredCount,
    pro: proCount,
    vip: vipCount,
    byCreator: includeBreakdown ? byCreator : undefined,
  });
});

app.post("/api/admin/vouchers", requireAdmin, async (req, res) => {
  if (process.env.REMOTE_SERVER_URL) {
    const remoteUrl = process.env.REMOTE_SERVER_URL.replace(/\/$/, "");
    try {
      const response = await fetch(`${remoteUrl}/api/admin/vouchers`, {
        method: "POST",
        headers: {
          "content-type": "application/json",
          "x-admin-password": req.headers["x-admin-password"]
        },
        body: JSON.stringify(req.body)
      });
      const data = await response.json().catch(() => ({}));
      if (response.ok) {
        // Simpan juga di lokal setelah sukses di remote
        try {
          // Penting: pakai data resellerInfo lokal jika request dari mitra
          // (remote mungkin tidak include createdBy/createdByName yang benar)
          const localCreatedBy = req.adminRole === "mitra"
            ? (req.resellerInfo.resellerId || data.voucher?.createdBy)
            : (data.voucher?.createdBy || "owner");
          const localCreatedByName = req.adminRole === "mitra"
            ? (req.resellerInfo.resellerName || data.voucher?.createdByName || "Mitra")
            : (data.voucher?.createdByName || "Owner");
          Vouchers.addVoucher({
            code: req.body.code,
            tier: req.body.tier,
            whatsapp: req.body.whatsapp,
            duration: req.body.duration,
            durationDays: req.body.durationDays,
            dailyLimit: req.body.dailyLimit,
            createdBy: localCreatedBy,
            createdByName: localCreatedByName,
            pricePaid: data.voucher?.pricePaid || req.body.pricePaid || 0,
            resellerCost: data.voucher?.resellerCost || 0,
            commission: data.voucher?.commission || 0,
          });
        } catch (_) { /* ignore local save error */ }
        return res.json(data);
      } else {
        return res.status(response.status).json(data);
      }
    } catch (e) {
      console.warn(`[Voucher] Gagal meneruskan ke remote (${e.message}), fallback ke lokal`);
      // Lanjut ke lokal jika remote tidak terjangkau
    }
  }

  try {
    const { code, tier, whatsapp, duration, durationDays, dailyLimit, pricePaid } = req.body || {};
    
    let createdBy = "owner";
    let createdByName = "Owner";
    let actualPricePaid = pricePaid;
    let resellerCost = 0;
    let commission = 0;
 
    const settings = OwnerSettings.getSettings();
    const isVip = (tier === "vip");

    let finalDays = 30;
    if (duration === "custom") {
      finalDays = parseInt(durationDays, 10);
      if (isNaN(finalDays) || finalDays <= 0) {
        return res.status(400).json({ error: "Masa aktif (hari) harus berupa angka positif" });
      }
    }
    const daysFactor = finalDays / 30;

    if (req.adminRole === "kasir") {
      createdBy = "kasir";
      createdByName = "Kasir";
      if (actualPricePaid === undefined || actualPricePaid === "") {
        const baseSrp = isVip ? (settings.prices.vipPrice || 500000) : settings.prices.proPrice;
        actualPricePaid = Math.round(baseSrp * daysFactor);
      } else {
        actualPricePaid = parseFloat(actualPricePaid) || 0;
      }
    } else if (req.adminRole === "mitra") {
      const resellerId = req.resellerInfo.resellerId;
      createdBy = resellerId;
      createdByName = req.resellerInfo.resellerName;
 
      const reseller = settings.resellers.find(r => r.id === resellerId);
      if (!reseller) {
        return res.status(400).json({ error: "Akun reseller tidak ditemukan." });
      }
 
      const baseResellerPrice = isVip
        ? (reseller.customResellerVipPrice !== null && reseller.customResellerVipPrice !== undefined ? reseller.customResellerVipPrice : (settings.prices.resellerVipPrice || 250000))
        : (reseller.customResellerPrice !== null && reseller.customResellerPrice !== undefined ? reseller.customResellerPrice : settings.prices.resellerPrice);
 
      const baseMinSrp = isVip ? (settings.prices.vipPrice || 500000) : settings.prices.proPrice;
      const baseActiveSrp = isVip
        ? (reseller.customVipSrp !== null && reseller.customVipSrp !== undefined ? reseller.customVipSrp : baseMinSrp)
        : (reseller.customSrp !== null && reseller.customSrp !== undefined ? reseller.customSrp : baseMinSrp);
 
      // Scale proportionally based on custom duration days relative to 30 days
      const activeResellerPrice = Math.round(baseResellerPrice * daysFactor);
      const minSrp = Math.round(baseMinSrp * daysFactor);
      const activeSrp = Math.round(baseActiveSrp * daysFactor);

      if (actualPricePaid === undefined || actualPricePaid === "") {
        actualPricePaid = activeSrp;
      } else {
        actualPricePaid = parseFloat(actualPricePaid) || 0;
      }
 
      if (actualPricePaid < minSrp) {
        return res.status(400).json({ error: `Dilarang menjual di bawah harga SRP Global! (Minimum: Rp ${minSrp.toLocaleString('id-ID')})` });
      }
 
      resellerCost = activeResellerPrice;
 
      // Check balance (with debt support)
      const currentBal = reseller.balance || 0;
      const debtOk = reseller.debtEnabled === true;
      const debtMax = reseller.debtLimit || 0;
      
      if (currentBal < activeResellerPrice) {
        if (!debtOk) {
          const tierName = isVip ? "VIP" : "Pro";
          return res.status(403).json({
            error: `Saldo deposit Anda tidak mencukupi untuk membuat voucher ${tierName}! (Dibutuhkan: Rp ${activeResellerPrice.toLocaleString('id-ID')}, Saldo Anda: Rp ${currentBal.toLocaleString('id-ID')}). Silakan hubungi Owner untuk topup.`
          });
        }
        // Debt enabled — check against limit
        const afterDeduction = currentBal - activeResellerPrice;
        if (Math.abs(afterDeduction) > debtMax) {
          const tierName = isVip ? "VIP" : "Pro";
          return res.status(403).json({
            error: `Batas utang terlampaui! Maksimal utang: Rp ${debtMax.toLocaleString('id-ID')}. Untuk membuat voucher ${tierName} dibutuhkan Rp ${activeResellerPrice.toLocaleString('id-ID')}, saldo Anda Rp ${currentBal.toLocaleString('id-ID')} (kekurangan Rp ${(activeResellerPrice - currentBal).toLocaleString('id-ID')} melebihi batas utang).`
          });
        }
      }
 
      // Deduct balance
      const deductResult = OwnerSettings.deductResellerBalance(resellerId, activeResellerPrice, actualPricePaid);
      if (!deductResult.ok) {
        return res.status(403).json({ error: deductResult.error });
      }
 
      commission = deductResult.commission;
      // Decrement legacy quota for backward compatibility
      OwnerSettings.decrementResellerQuota(resellerId);
    } else {
      // Owner
      if (actualPricePaid === undefined || actualPricePaid === "") {
        const baseSrp = isVip ? (settings.prices.vipPrice || 500000) : settings.prices.proPrice;
        actualPricePaid = Math.round(baseSrp * daysFactor);
      } else {
        actualPricePaid = parseFloat(actualPricePaid) || 0;
      }
    }
    
    let finalDailyLimit = parseInt(dailyLimit, 10);
    if (!finalDailyLimit || isNaN(finalDailyLimit)) {
      finalDailyLimit = (tier === "vip") ? 8 : 5;
    }

    const vch = Vouchers.addVoucher({ 
      code, 
      tier, 
      whatsapp, 
      duration, 
      durationDays, 
      dailyLimit: finalDailyLimit,
      createdBy,
      createdByName,
      pricePaid: actualPricePaid,
      resellerCost,
      commission
    });

    // Auto-log financial transaction
    const logCategory = tier === "vip" ? "vip" : "pro";
    if (req.adminRole === "mitra") {
      // Mitra sale: log commission income
      Finance.addTransaction({
        type: "voucher_sale",
        actor: req.resellerInfo.resellerId,
        actorName: createdByName,
        amount: commission, // commission = profit for reseller
        description: `Penjualan voucher ${tier.toUpperCase()} ke ${vch.whatsapp} (kode: ${vch.code}) — Komisi`,
        voucherCode: vch.code,
        relatedResellerId: req.resellerInfo.resellerId,
        category: logCategory,
        meta: { pricePaid: actualPricePaid, resellerCost, durationDays: vch.durationDays },
      });

      // Log debt entry if balance went negative
      if (deductResult && deductResult.isDebt) {
        Finance.addTransaction({
          type: "debt",
          actor: req.resellerInfo.resellerId,
          actorName: createdByName,
          amount: -deductResult.debtAmount,
          description: `Pembuatan voucher ${tier.toUpperCase()} ${vch.code} menggunakan utang — Total utang: ${Finance.formatCurrency(deductResult.debtAmount)}`,
          voucherCode: vch.code,
          relatedResellerId: req.resellerInfo.resellerId,
          category: logCategory,
          meta: { debtAmount: deductResult.debtAmount, debtLimit: deductResult.debtLimit, balanceAfter: deductResult.balance },
        });
      }

      // Owner also gets the resellerCost portion
      Finance.addTransaction({
        type: "voucher_sale",
        actor: "owner",
        actorName: "Owner",
        amount: resellerCost,
        description: `Pendapatan dari mitra ${createdByName} — voucher ${tier.toUpperCase()} ${vch.code} (${vch.whatsapp})`,
        voucherCode: vch.code,
        relatedResellerId: req.resellerInfo.resellerId,
        category: logCategory,
        meta: { pricePaid: actualPricePaid, resellerCost, via: "mitra", mitraName: createdByName },
      });
    } else {
      // Owner direct sale: full pricePaid is revenue
      Finance.addTransaction({
        type: "voucher_sale",
        actor: "owner",
        actorName: "Owner",
        amount: actualPricePaid,
        description: `Penjualan langsung voucher ${tier.toUpperCase()} ke ${vch.whatsapp} (kode: ${vch.code})`,
        voucherCode: vch.code,
        category: logCategory,
        meta: { durationDays: vch.durationDays },
      });
    }

    const responsePayload = { voucher: vch };
    if (req.adminRole === "mitra") {
      const freshSettings = OwnerSettings.getSettings();
      const freshReseller = freshSettings.resellers.find(r => r.id === req.resellerInfo.resellerId);
      if (freshReseller) {
        responsePayload.updatedReseller = {
          balance: freshReseller.balance,
          totalCommission: freshReseller.totalCommission,
          quota: freshReseller.quota,
          isDebt: freshReseller.balance < 0,
          debtAmount: freshReseller.balance < 0 ? Math.abs(freshReseller.balance) : 0,
          debtLimit: freshReseller.debtEnabled ? (freshReseller.debtLimit || 0) : 0,
          debtEnabled: freshReseller.debtEnabled || false,
          debtWarning: freshReseller.debtEnabled && freshReseller.balance < 0 && (freshReseller.debtLimit || 0) > 0 && Math.abs(freshReseller.balance) >= (freshReseller.debtLimit || 0) * 0.9,
        };
      }
    }
    res.json(responsePayload);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

app.delete("/api/admin/vouchers/:code", requireAdmin, async (req, res) => {
  const code = req.params.code.trim().toUpperCase();

  // Validasi password delete
  const deletePassword = req.headers["x-delete-password"];
  if (!deletePassword) {
    return res.status(401).json({ error: "Password wajib diisi untuk menghapus voucher!" });
  }
  const pwValidation = OwnerSettings.validateLogin(deletePassword);
  if (!pwValidation.ok) {
    return res.status(403).json({ error: "Password salah! Tidak dapat menghapus voucher." });
  }
  // Pastikan role yang validasi sesuai dengan role session
  if (req.adminRole !== pwValidation.role) {
    return res.status(403).json({ error: "Password tidak sesuai dengan akun login Anda!" });
  }

  // Mitra: cek otorisasi dulu (berlaku untuk lokal & remote)
  if (req.adminRole === "mitra") {
    const allVouchers = Vouchers.listVouchers();
    const v = allVouchers.find((x) => x.code === code);
    if (v) {
      const myId = req.resellerInfo.resellerId;
      const myName = (req.resellerInfo.resellerName || "").toLowerCase().trim();
      const vName = (v.createdByName || "").toLowerCase().trim();
      const isOwner = v.createdBy === myId || (myName && vName && vName === myName);
      if (!isOwner) {
        return res.status(403).json({ error: "Akses Ditolak: Anda hanya dapat menghapus voucher Anda sendiri." });
      }
    }
    // Hapus lokal dulu
    const localOk = Vouchers.deleteVoucher(code);
    // Coba hapus remote juga (kalau ada)
    if (process.env.REMOTE_SERVER_URL) {
      try {
        const remoteUrl = process.env.REMOTE_SERVER_URL.replace(/\/$/, "");
        await fetch(`${remoteUrl}/api/admin/vouchers/${encodeURIComponent(code)}`, {
          method: "DELETE",
          headers: { "x-admin-password": req.headers["x-admin-password"] }
        });
      } catch (_) { /* ignore remote error */ }
    }
    return res.json({ ok: true, deleted: true });
  }

  // Owner/kasir: proxy ke remote dulu
  if (process.env.REMOTE_SERVER_URL) {
    const remoteUrl = process.env.REMOTE_SERVER_URL.replace(/\/$/, "");
    try {
      const response = await fetch(`${remoteUrl}/api/admin/vouchers/${encodeURIComponent(code)}`, {
        method: "DELETE",
        headers: { "x-admin-password": req.headers["x-admin-password"] }
      });
      const data = await response.json().catch(() => ({}));
      if (response.ok) {
        return res.json(data);
      }
      // Remote gagal — fallback ke local
      console.warn(`[Delete] Remote sync gagal (HTTP ${response.status}), fallback ke local`);
    } catch (e) {
      console.warn(`[Delete] Remote unreachable: ${e.message}, fallback ke local`);
    }
  }

  const ok = Vouchers.deleteVoucher(code);
  if (!ok) return res.status(404).json({ error: "Not found" });
  res.json({ ok: true });
});

app.put("/api/admin/vouchers/:code", requireAdmin, async (req, res) => {
  if (process.env.REMOTE_SERVER_URL) {
    const remoteUrl = process.env.REMOTE_SERVER_URL.replace(/\/$/, "");
    try {
      const response = await fetch(`${remoteUrl}/api/admin/vouchers/${encodeURIComponent(req.params.code)}`, {
        method: "PUT",
        headers: {
          "content-type": "application/json",
          "x-admin-password": req.headers["x-admin-password"]
        },
        body: JSON.stringify(req.body)
      });
      const data = await response.json().catch(() => ({}));
      if (response.ok) {
        return res.json(data);
      } else {
        return res.status(response.status).json(data);
      }
    } catch (e) {
      return res.status(500).json({ error: `Gagal meneruskan ke remote: ${e.message}` });
    }
  }

  const settings = OwnerSettings.getSettings();
  let existingVoucher = null;
  if (req.adminRole === "mitra") {
    const allVouchers = Vouchers.listVouchers();
    existingVoucher = allVouchers.find((x) => x.code === req.params.code.trim().toUpperCase());
    if (!existingVoucher || existingVoucher.createdBy !== req.resellerInfo.resellerId) {
      return res.status(403).json({ error: "Akses Ditolak: Anda hanya dapat mengubah voucher Anda sendiri." });
    }
  }
  try {
    const { tier, whatsapp, dailyLimit, expiresAt, pricePaid } = req.body || {};
    
    if (req.adminRole === "mitra" && pricePaid !== undefined && pricePaid !== null && pricePaid !== "") {
      const activeTier = tier || existingVoucher?.tier || "pro";
      const isVip = activeTier === "vip";
      const baseMinSrp = isVip ? (settings.prices.vipPrice || 500000) : settings.prices.proPrice;

      const addedAt = existingVoucher?.addedAt ? new Date(existingVoucher.addedAt) : new Date();
      const targetExpiresAt = expiresAt ? new Date(expiresAt) : (existingVoucher?.expiresAt ? new Date(existingVoucher.expiresAt) : new Date());
      const diffMs = targetExpiresAt.getTime() - addedAt.getTime();
      const diffDays = Math.max(1, Math.ceil(diffMs / (24 * 60 * 60 * 1000)));
      const daysFactor = diffDays / 30;

      const minSrp = Math.round(baseMinSrp * daysFactor);
      if (parseFloat(pricePaid) < minSrp) {
        return res.status(400).json({ error: `Dilarang menjual di bawah harga SRP Global! (Minimum: Rp ${minSrp.toLocaleString('id-ID')})` });
      }
    }

    let finalDailyLimit = dailyLimit !== undefined ? parseInt(dailyLimit, 10) : undefined;
    if (tier !== undefined && (finalDailyLimit === undefined || isNaN(finalDailyLimit))) {
      finalDailyLimit = (tier === "vip") ? 8 : 5;
    }
    const vch = Vouchers.updateVoucher(req.params.code, { tier, whatsapp, dailyLimit: finalDailyLimit, expiresAt, pricePaid });
    res.json({ voucher: vch });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// ═══ Trial Voucher — Auto + Custom (tanpa potong saldo) ═══

app.post("/api/admin/trial-voucher", requireAdmin, async (req, res) => {
  try {
    const { customName } = req.body || {};

    let createdBy = "owner";
    let createdByName = "Owner";

    if (req.adminRole === "mitra") {
      createdBy = req.resellerInfo.resellerId;
      createdByName = req.resellerInfo.resellerName || "Mitra";
    } else if (req.adminRole === "kasir") {
      createdBy = "kasir";
      createdByName = "Kasir";
    }

    // Forward ke remote jika ada
    if (process.env.REMOTE_SERVER_URL) {
      const remoteUrl = process.env.REMOTE_SERVER_URL.replace(/\/$/, "");
      try {
        const response = await fetch(`${remoteUrl}/api/admin/trial-voucher`, {
          method: "POST",
          headers: {
            "content-type": "application/json",
            "x-admin-password": req.headers["x-admin-password"],
          },
          body: JSON.stringify({ customName, createdBy, createdByName }),
        });
        const data = await response.json().catch(() => ({}));
        if (response.ok) {
          // Simpan lokal juga
          try {
            Vouchers.addTrialVoucher({ createdBy, createdByName, customName });
          } catch (_) {}
          return res.json(data);
        }
        // Remote gagal — fallback ke local creation
        console.warn(`[Trial] Remote sync gagal (HTTP ${response.status}), fallback ke local`);
      } catch (e) {
        console.warn(`[Trial] Gagal forward ke remote: ${e.message}`);
      }
    }

    const result = Vouchers.addTrialVoucher({ createdBy, createdByName, customName });

    // Log aktivitas trial voucher
    try {
      const { logAccessAction } = await import("./lib/access-log-store.js");
      logAccessAction({
        type: "access",
        actor: createdBy,
        actorName: createdByName,
        action: "Buat Trial Voucher",
        targetResellerId: createdBy,
        targetResellerName: createdByName,
        description: `${createdByName} membuat trial voucher "${result.voucher.trialName}" (kode: ${result.voucher.code}, kuota tersisa: ${result.remainingQuota}/5)`,
        result: "success",
        ip: req.ip || "",
      });
    } catch (_) {}

    res.json({
      ok: true,
      voucher: result.voucher,
      remainingQuota: result.remainingQuota,
      totalUsed: result.totalUsed,
    });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// Cek kuota trial yang tersisa
app.get("/api/admin/trial-quota", requireAdmin, (req, res) => {
  try {
    let createdBy = "owner";
    if (req.adminRole === "mitra") {
      createdBy = req.resellerInfo.resellerId;
    } else if (req.adminRole === "kasir") {
      createdBy = "kasir";
    }
    const remaining = Vouchers.getTrialQuota(createdBy);
    const used = Vouchers.getTrialCount(createdBy);
    res.json({ ok: true, remainingQuota: remaining, totalUsed: used, maxQuota: 3 });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// ─── Public config ──────────────────────────────────────────────────────────

app.get("/api/config", (_req, res) => {
  const accs = Accounts.listAccountsSafe({ includeAll: true });
  const available = accs.filter((a) => !a.expired && !a.archived).length;
  const settings = OwnerSettings.getSettings();
  res.json({
    defaultRecipeId: DEFAULT_RECIPE_ID,
    totalAccounts: accs.length,
    availableAccounts: available,
    archivedAccounts: accs.filter((a) => a.archived).length,
    maintenance: settings.maintenance || {
      active: false,
      title: "Server Sedang Pemeliharaan",
      description: "Kami sedang melakukan peningkatan sistem. Generator diistirahatkan sementara untuk memastikan kestabilan layanan.",
      reason: "",
      duration: "",
      expiresAt: null
    }
  });
});

app.get("/api/admin/maintenance", requireAdmin, (_req, res) => {
  try {
    const settings = OwnerSettings.getSettings();
    res.json(settings.maintenance || { active: false, reason: "", duration: "", expiresAt: null });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.post("/api/admin/maintenance", requireAdmin, (req, res) => {
  try {
    const { active, reason, duration, expiresAt, title, description } = req.body || {};
    const settings = OwnerSettings.getSettings();
    
    settings.maintenance = {
      active: Boolean(active),
      title: String(title || "Server Sedang Pemeliharaan").trim(),
      description: String(description || "Kami sedang melakukan peningkatan sistem. Generator diistirahatkan sementara untuk memastikan kestabilan layanan.").trim(),
      reason: String(reason || "Pemeliharaan sistem berkala").trim(),
      duration: String(duration || "1 jam").trim(),
      expiresAt: expiresAt || null
    };

    OwnerSettings.writeSettings(settings);
    res.json({ ok: true, maintenance: settings.maintenance });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Local Promo Media Upload
app.post("/api/admin/promo/upload", requireAdmin, upload.single("file"), (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: "No file provided" });
    }

    const ext = path.extname(req.file.originalname).toLowerCase();
    const allowedExts = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".mp4", ".mov", ".webm", ".ogg"];
    if (!allowedExts.includes(ext)) {
      return res.status(400).json({ error: "Format file tidak didukung (gunakan gambar/video standar)" });
    }

    const uploadDir = path.join(__dirname, "public", "uploads", "promo");
    if (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir, { recursive: true });
    }

    const fileName = "promo_" + Date.now() + "_" + Math.random().toString(36).substring(2, 8) + ext;
    const filePath = path.join(uploadDir, fileName);

    fs.writeFileSync(filePath, req.file.buffer);

    res.json({
      ok: true,
      url: `/uploads/promo/${fileName}`,
      mediaType: [".mp4", ".mov", ".webm", ".ogg"].includes(ext) ? "video" : "image"
    });
  } catch (err) {
    console.error("Local promo upload error:", err);
    res.status(500).json({ error: err.message });
  }
});

// Get Promo Settings
app.get("/api/admin/promo/settings", requireAdmin, (req, res) => {
  try {
    const settings = OwnerSettings.getSettings();
    if (req.adminRole === "owner" || req.adminRole === "kasir") {
      res.json({ ok: true, promo: settings.globalPromo || { active: false, title: "", btnText: "", btnLink: "", mediaUrl: "", mediaType: "image", description: "" } });
    } else if (req.adminRole === "mitra") {
      const resellerId = req.resellerInfo.resellerId;
      const reseller = settings.resellers.find(r => r.id === resellerId);
      if (!reseller) return res.status(404).json({ error: "Reseller tidak ditemukan" });
      res.json({ ok: true, promo: reseller.promo || { active: false, title: "", btnText: "", btnLink: "", mediaUrl: "", mediaType: "image", description: "" } });
    } else {
      res.status(403).json({ error: "Akses ditolak" });
    }
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Save Promo Settings
app.post("/api/admin/promo/settings", requireAdmin, (req, res) => {
  try {
    const { active, title, btnText, btnLink, mediaUrl, mediaType, description } = req.body || {};
    const settings = OwnerSettings.getSettings();

    const promoData = {
      active: Boolean(active),
      title: String(title || "").trim(),
      btnText: String(btnText || "").trim(),
      btnLink: String(btnLink || "").trim(),
      mediaUrl: String(mediaUrl || "").trim(),
      mediaType: String(mediaType || "image").trim(),
      description: String(description || "").trim()
    };

    if (req.adminRole === "owner" || req.adminRole === "kasir") {
      settings.globalPromo = promoData;
      OwnerSettings.writeSettings(settings);
      res.json({ ok: true, promo: promoData });
    } else if (req.adminRole === "mitra") {
      const resellerId = req.resellerInfo.resellerId;
      const resellerIdx = settings.resellers.findIndex(r => r.id === resellerId);
      if (resellerIdx < 0) return res.status(404).json({ error: "Reseller tidak ditemukan" });

      settings.resellers[resellerIdx].promo = promoData;
      OwnerSettings.writeSettings(settings);
      res.json({ ok: true, promo: promoData });
    } else {
      res.status(403).json({ error: "Akses ditolak" });
    }
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

function getBusyAccountIds() {
  const busyIds = new Set();
  for (const job of activeJobs.values()) {
    if (job.status === "RUNNING" || job.status === "PENDING") {
      busyIds.add(job.accountId);
    }
  }
  return busyIds;
}

async function getOrReviveAccount(excludeIds = null) {
  const tried = excludeIds instanceof Set ? excludeIds : new Set(excludeIds || []);
  
  // Exclude busy accounts that are currently generating a video
  const busyIds = getBusyAccountIds();
  for (const id of busyIds) {
    tried.add(id);
  }
  
  // 1. Coba ambil akun yang aktif sesuai batas runs bulanan saat ini
  let acc = Accounts.pickAccount(tried);
  if (acc) return acc;

  // 2. Jika tidak ada yang aktif, cari akun expired non-archive yang bisa di-revive
  const allAccounts = Accounts.listAccounts();
  const revivable = allAccounts.filter(a => 
    !a.archived &&
    !a.figmaBlocked && !a.googleBlocked &&
    a.password &&
    (Accounts.isExpired(a.token) || a.expired) &&
    (a.runs || 0) < (a.maxRuns || Accounts.DEFAULT_MAX_RUNS) &&
    a.email &&
    !tried.has(a.id)
  );

  if (revivable.length === 0) {
    return null; // Tidak ada akun yang bisa di-revive
  }

  // Prioritaskan akun yang runs-nya paling tinggi agar akun yang hampir habis jatahnya dipakai duluan
  revivable.sort((a, b) => {
    const runsA = a.runs || 0;
    const runsB = b.runs || 0;
    return runsB - runsA;
  });

  for (const target of revivable) {
    console.log(`[Generate-Trigger] Mencoba revive akun ${target.email} (runs: ${target.runs || 0})...`);
    broadcastLog({ type: "info", msg: `🔄 [Generate-Trigger] Mengaktifkan kembali akun: ${target.email} (runs: ${target.runs || 0})...` });

    try {
      const result = await refreshExistingAccountToken(target.id, target.email, target.password, (event) => broadcastLog(event));
      if (result.ok) {
        broadcastLog({ type: "success", msg: `✅ [Generate-Trigger] Akun ${target.email} berhasil di-revive!` });
        // Ambil data akun terbaru dari disk yang sudah terupdate tokennya
        const updatedAcc = Accounts.getAccount(target.id);
        if (updatedAcc && !updatedAcc.expired && !Accounts.isExpired(updatedAcc.token)) {
          return updatedAcc;
        }
      } else {
        broadcastLog({ type: "error", msg: `❌ [Generate-Trigger] Gagal revive akun ${target.email}: ${result.error}` });
      }
    } catch (err) {
      console.error(`[Generate-Trigger] Error revive ${target.email}:`, err);
      broadcastLog({ type: "error", msg: `❌ [Generate-Trigger] Error revive ${target.email}: ${err.message}` });
    }
  }

  return null;
}

// ─── User-facing API: upload + generate (auto-pick account) ─────────────────

app.post("/api/upload", requireVoucher, upload.single("file"), async (req, res) => {
  try {
    if (!req.file) return res.status(400).json({ error: "No file provided" });

    const acc = await getOrReviveAccount();
    if (!acc) return res.status(503).json({ error: "Server sedang tidak tersedia. Silakan hubungi Admin untuk bantuan." });

    console.log(`[Upload] Uploading ${req.file.originalname} (${req.file.size} bytes) to tmpfiles.org...`);

    // 1. Upload ke tmpfiles.org
    const TMPFILES_API = "https://tmpfiles.org/api/v1/upload";
    const boundary = "----TmpfilesBoundary" + Math.random().toString(36).substring(2, 15);
    const crlf = "\r\n";
    const header = [
      `--${boundary}`,
      `Content-Disposition: form-data; name="file"; filename="${req.file.originalname}"`,
      `Content-Type: ${req.file.mimetype}`,
      ``,
    ].join(crlf);
    const footer = `--${boundary}--`;
    const bodyBuffer = Buffer.concat([
      Buffer.from(header + crlf, "utf-8"),
      req.file.buffer,
      Buffer.from(crlf + footer, "utf-8"),
    ]);

    const upResp = await fetch(TMPFILES_API, {
      method: "POST",
      headers: { "Content-Type": `multipart/form-data; boundary=${boundary}` },
      body: bodyBuffer,
    });

    if (!upResp.ok) {
      const errText = await upResp.text();
      console.error("[Upload] Tmpfiles upload failed:", upResp.status, errText);
      return res.status(upResp.status).json({ error: `Gagal mengunggah file ke server penyimpanan sementara (HTTP ${upResp.status})` });
    }

    const upData = await upResp.json();
    if (upData?.status !== "success" || !upData?.data?.url) {
      console.error("[Upload] Tmpfiles response error:", upData);
      return res.status(500).json({ error: "Format respon penyimpanan sementara tidak valid" });
    }

    let tmpUrl = upData.data.url.replace(/^https?:\/\//, "https://");
    tmpUrl = tmpUrl.replace(/^https:\/\/tmpfiles\.org\/(?!dl\/)(.+)$/, "https://tmpfiles.org/dl/$1");
    
    console.log(`[Upload] Tmpfiles upload success: ${tmpUrl}`);

    // 2. Buat mock asset object yang kompatibel dengan Weavy response
    const mockAsset = {
      id: "asset_tmp_" + Math.random().toString(36).substring(2, 15),
      name: req.file.originalname,
      url: tmpUrl,
      width: req.file.mimetype.startsWith("image/") ? 1024 : 576,
      height: req.file.mimetype.startsWith("image/") ? 1024 : 1024,
      mimeType: req.file.mimetype,
    };

    const maskedData = registerAndMaskUploadResponse(mockAsset, acc.token);
    res.json({ ...maskedData, _accountUsed: acc.label });
  } catch (err) {
    console.error("Upload error:", err);
    res.status(500).json({ error: err.message });
  }
});

app.post("/api/generate", requireVoucher, async (req, res) => {
  try {
    let {
      imageUrl, imageMeta,
      videoUrl, videoMeta,
      prompt = "",
      modelType = "Standard",
      version = "V2.6",
      keepOriginalSound = true,
      characterOrientation = "video",
      numberOfRuns = 1,
      excludeIds = [],
      jobId: clientJobId,
    } = req.body || {};

    const restoredImage = restoreAssetDetail(imageUrl, imageMeta);
    imageUrl = restoredImage.url;
    imageMeta = restoredImage.meta;

    const restoredVideo = restoreAssetDetail(videoUrl, videoMeta);
    videoUrl = restoredVideo.url;
    videoMeta = restoredVideo.meta;

    if (!imageUrl || !videoUrl) return res.status(400).json({ error: "imageUrl and videoUrl required" });
    if (!["image", "video"].includes(characterOrientation)) return res.status(400).json({ error: "invalid characterOrientation" });
    if (!["Pro", "Standard"].includes(modelType)) return res.status(400).json({ error: "invalid modelType" });
    if (!["V2.6", "V3.0"].includes(version)) return res.status(400).json({ error: "invalid version" });

    // Enforce Standard tier restrictions
    if (req.voucher.tier === "standard") {
      if (modelType !== "Standard" || version !== "V2.6") {
        return res.status(403).json({ error: "Fitur Terkunci: Upgrade voucher Anda ke plan Pro untuk menggunakan Model Pro dan Versi 3.0." });
      }
    }

    // Check daily generate limit
    const todayStr = new Date().toISOString().split("T")[0];
    const currentUsage = req.voucher.usage?.[todayStr] || 0;
    const limit = req.voucher.dailyLimit || 3;
    if (currentUsage >= limit) {
      return res.status(403).json({ error: `Jatah generate harian Anda telah habis (${currentUsage}/${limit} digunakan hari ini). Harap coba lagi besok.` });
    }

    // Retry with up to 3 accounts if one is unauthorized
    const triedIds = new Set(excludeIds);
    let lastError = null;
    let attempts = 0;

    while (attempts < 3) {
      // Don't exclude the same account yet — we may retry with a new recipe
      const acc = await getOrReviveAccount(attempts > 0 ? triedIds : null);
      if (!acc) break;

      broadcastLog({
        type: "info",
        msg: `🎯 [Generate] Memilih akun: ${acc.email || acc.label} (runs: ${acc.runs || 0}, archived: ${acc.archived ? "yes" : "no"})`,
      });

      const recipeId = acc.recipeId || DEFAULT_RECIPE_ID;

      // Fetch recipe nodes dynamically to avoid UUID mismatch
      let recipeNodes = null;
      let recipeAutoCreated = false;
      try {
        const recipeRes = await weavyFetch(`/api/v1/recipes/${recipeId}`, {
          method: "GET",
          token: acc.token,
          extraHeaders: { "x-app-recipeid": recipeId },
        });
        if (recipeRes.status === 401) {
          console.warn(`Account ${acc.label} got 401 on recipe check, marking expired...`);
          broadcastLog({
            type: "warn",
            msg: `⚠️ [Generate] Akun ${acc.email || acc.label} kena 401 saat cek resep, ditandai expired lalu coba akun lain.`,
          });
          Accounts.updateAccount(acc.id, { expired: true });
          triedIds.add(acc.id);
          attempts++;
          lastError = "Authentication Error";
          continue;
        }
        if (recipeRes.status === 403 || recipeRes.status === 404) {
          // Recipe doesn't belong to this account or not found — auto-create one
          console.warn(`Account ${acc.label} got ${recipeRes.status} on recipe ${recipeId}, auto-creating recipe...`);
          broadcastLog({
            type: "warn",
            msg: `⚠️ [Generate] Akun ${acc.email || acc.label} tidak punya akses ke recipe (403). Mencoba buat recipe baru...`,
          });
          try {
            const newRecipeId = await ensureKling26Recipe(acc.token, null, (msg) => {
              broadcastLog({ type: "info", msg: `🔧 [Generate-Recipe] ${msg}` });
            });
            if (newRecipeId) {
              Accounts.updateAccount(acc.id, { recipeId: newRecipeId });
              acc.recipeId = newRecipeId;
              recipeAutoCreated = true;
              console.log(`[Generate] Created new recipe ${newRecipeId} for ${acc.label}, retrying...`);
              broadcastLog({
                type: "info",
                msg: `✅ [Generate] Recipe baru dibuat (${newRecipeId.slice(0, 12)}...) untuk ${acc.email || acc.label}, melanjutkan generate.`,
              });
              // DON'T increment attempts — retry with same account + new recipe
              // Loop will naturally re-enter with acc.recipeId now set
            }
          } catch (recipeErr) {
            console.error(`[Generate] Failed to create recipe for ${acc.label}:`, recipeErr.message);
          }
          // If recipe auto-create failed OR we fell through, skip to next account
          if (!recipeAutoCreated) {
            Accounts.updateAccount(acc.id, { expired: true });
            triedIds.add(acc.id);
            attempts++;
            lastError = "Recipe access denied";
            continue;
          }
        }
        // If we auto-created the recipe, re-enter loop (acc.recipeId is now set)
        if (recipeAutoCreated) continue;

        if (recipeRes.ok) {
          const recipeData = await recipeRes.json();
          const nodes = recipeData.nodes || [];
          const edges = recipeData.edges || [];

          const modelNode = nodes.find(n => {
            if (!n.type && !n.isModel) return false;
            const t = n.type || "";
            const name = (n.name || n.label || "").toLowerCase();
            // Hanya ambil model fal-ai/kling — abaikan custom model lain
            return t === "custommodelV2" || n.isModel && (
              name.includes("kling") || name.includes("fal-ai") || name.includes("motion")
            );
          }) || nodes.find(n => n.type === "custommodelV2" || n.isModel);
          if (modelNode) {
            const modelId = modelNode.id;
            const groupId = modelNode.parentId || "";

            const imageEdge = edges.find(e => e.target === modelId && (e.targetHandle?.includes("image") || e.targetHandle?.includes("image_url")));
            const imageId = imageEdge ? imageEdge.source : null;

            const videoEdge = edges.find(e => e.target === modelId && (e.targetHandle?.includes("video") || e.targetHandle?.includes("video_url")));
            const videoId = videoEdge ? videoEdge.source : null;

            if (imageId && videoId) {
              recipeNodes = {
                model: modelId,
                image: imageId,
                video: videoId,
                group: groupId
              };
              console.log(`[Generate] Extracted node IDs for ${acc.label}:`, recipeNodes);
            }
          }
        }
      } catch (err) {
        console.warn(`[Generate] Failed to fetch recipe nodes for ${acc.label}: ${err.message}`);
      }

      if (!recipeNodes) {
        console.warn(`[Generate] Fallback to default node IDs for ${acc.label}`);
      }

      const payload = buildExecutePayload({
        imageUrl, imageMeta, videoUrl, videoMeta,
        prompt, modelType, version, keepOriginalSound, characterOrientation, numberOfRuns,
        recipeNodes,
      });

      const r = await weavyFetch(`/api/v1/batches/recipes/${recipeId}/execute`, {
        method: "POST",
        token: acc.token,
        body: payload,
        extraHeaders: { "x-app-recipeid": recipeId },
      });

      const data = await readResponse(r);

      if (r.ok) {
        const batchId = data.json?.batchId || data.json?.id || "";
        const jobId = addJob(acc.id, batchId, recipeId, req.voucher.code, { imageUrl, imageMeta, videoUrl, videoMeta, prompt, modelType, version, keepOriginalSound, characterOrientation, numberOfRuns, recipeNodes }, clientJobId);
        return res.json({
          ...data.json,
          _accountId: acc.id,
          _accountLabel: acc.label,
          _recipeId: recipeId,
          jobId: jobId,
          batchId: jobId,
        });
      }

      // 401 = token truly invalid → mark expired on disk
      // 403 = recipe access issue → skip for this request only (don't permanently expire)
      if (r.status === 401) {
        console.warn(`Account ${acc.label} got 401 (invalid token), marking expired...`);
        broadcastLog({
          type: "warn",
          msg: `⚠️ [Generate] Akun ${acc.email || acc.label} kena 401 token invalid, ditandai expired lalu coba akun lain.`,
        });
        Accounts.updateAccount(acc.id, { expired: true });
        triedIds.add(acc.id);
        attempts++;
        lastError = extractWeavyError(data);
        continue;
      }

      if (r.status === 403) {
        if (data.json && data.json.internalErrorCode === 1076) {
          console.log(`[Generate] Model unverified detected for account ${acc.label}. Attempting auto-approval...`);
          broadcastLog({
            type: "info",
            msg: `🔓 [Generate] Model unverified terdeteksi untuk akun ${acc.email || acc.label}. Mencoba menyetujui model (auto-approve)...`
          });

          try {
            const approveRes = await weavyFetch("/api/v1/workspaces/models/approve", {
              method: "POST",
              token: acc.token,
              body: { modelIds: ["kwaivgi/kling-v2.6-motion-control"] },
              extraHeaders: { "x-app-recipeid": recipeId }
            });

            if (approveRes.ok) {
              console.log(`[Generate] Auto-approval succeeded for account ${acc.label}. Retrying execution...`);
              broadcastLog({
                type: "success",
                msg: `✅ [Generate] Model berhasil disetujui untuk ${acc.email || acc.label}. Mengulangi proses generate...`
              });

              // Retry execution
              const retryR = await weavyFetch(`/api/v1/batches/recipes/${recipeId}/execute`, {
                method: "POST",
                token: acc.token,
                body: payload,
                extraHeaders: { "x-app-recipeid": recipeId },
              });

              const retryData = await readResponse(retryR);

              if (retryR.ok) {
                const batchId = retryData.json?.batchId || retryData.json?.id || "";
                const jobId = addJob(acc.id, batchId, recipeId, req.voucher.code, { imageUrl, imageMeta, videoUrl, videoMeta, prompt, modelType, version, keepOriginalSound, characterOrientation, numberOfRuns, recipeNodes }, clientJobId);
                return res.json({
                  ...retryData.json,
                  _accountId: acc.id,
                  _accountLabel: acc.label,
                  _recipeId: recipeId,
                  jobId: jobId,
                  batchId: jobId,
                });
              } else {
                console.error("Retry failed after approval:", retryR.status, retryData.json || retryData.text);
                lastError = extractWeavyError(retryData);
              }
            } else {
              const approveErrText = await approveRes.text();
              console.error(`[Generate] Auto-approval failed for account ${acc.label}:`, approveRes.status, approveErrText);
              lastError = `Gagal menyetujui model: ${approveRes.status} ${approveErrText}`;
            }
          } catch (approveErr) {
            console.error(`[Generate] Error during auto-approval for account ${acc.label}:`, approveErr);
            lastError = `Error auto-approve: ${approveErr.message}`;
          }
        } else {
          console.warn(`Account ${acc.label} got 403 (recipe access denied), trying next...`);
          broadcastLog({
            type: "warn",
            msg: `⚠️ [Generate] Akun ${acc.email || acc.label} kena 403/akses recipe ditolak, skip ke akun lain.`,
          });
          lastError = extractWeavyError(data);
        }
        triedIds.add(acc.id);
        attempts++;
        continue;
      }

      // Other errors (credit limit, server error, etc.) → auto-rotate to next account
      const errText = (data.json?.error || data.text || data.json?.message || "").toString();
      console.error(`Generate failed (${r.status}) for ${acc.label}:`, errText.substring(0, 200));
      const isCreditIssue = /credit|insufficient|balance|billing|limit|insufficient credits/i.test(errText);
      broadcastLog({
        type: "warn",
        msg: `⚠️ [Generate] ${isCreditIssue ? "Akun kehabisan kredit" : "Generate gagal"} (${r.status}) pada ${acc.email || acc.label}: ${errText.substring(0, 80)}. ${attempts < 2 ? "Mencoba akun pool lain..." : ""}`,
      });
      triedIds.add(acc.id);
      attempts++;
      lastError = extractWeavyError(data);
      continue;
    }

    // All accounts tried
    if (attempts === 0) {
      return res.status(503).json({ error: "Server sedang tidak tersedia. Silakan hubungi Admin untuk bantuan." });
    }
    return res.status(403).json({
      error: lastError || "Server sedang sibuk, silakan coba lagi nanti atau hubungi Admin.",
      triedAccounts: attempts,
    });

  } catch (err) {
    console.error("Generate error:", err);
    res.status(500).json({ error: err.message });
  }
});


async function handlePoll(req, res) {
  let { jobId, accountId, batchId } = req.params;
  let recipeId = req.query.recipeId;

  if (jobId && jobId.startsWith("job_")) {
    const job = activeJobs.get(jobId);
    if (!job) {
      return res.status(404).json({ error: "Proses generate tidak ditemukan atau sudah kedaluwarsa." });
    }
    // Return server-side cached status immediately
    return res.json({
      found: job.status === "COMPLETED" && Boolean(job.url),
      url: job.url,
      duration: job.duration || null,
      status: job.status,
      error: job.error || null,
      recipeId: job.recipeId,
      batchId: job.batchId,
      progress: job.progress || 0,
    });
  }

  if (!accountId || !batchId) {
    return res.status(400).json({ error: "Parameter tidak lengkap." });
  }

  const acc = Accounts.getAccount(accountId);
  if (!acc) return res.status(404).json({ error: "Account not found" });

  const finalRecipeId = recipeId || acc.recipeId || DEFAULT_RECIPE_ID;

  try {
    const result = await pollBatchStatus({ token: acc.token, recipeId: finalRecipeId, batchId });

    if (!result.ok) {
      return res.status(result.status || 500).json({ error: result.error || "Status check failed" });
    }

    if (result.status === "FAILED") {
      const errText = String(result.error || "").toLowerCase();
      if (errText.includes("credit") || errText.includes("insufficient") || errText.includes("limit") || errText.includes("balance") || errText.includes("billing")) {
        console.warn(`Account ${acc.label} reported credit/limit issue during polling...`);
      }

    }

    let maskedUrl = null;
    if (result.url) {
      // Upload ke tmpfiles.org untuk durasi 1 jam yang pasti
      let stableUrl = result.url;
      try {
        stableUrl = await uploadToTmpfiles(result.url);
      } catch (_e) { /* fallback ke original */ }

      const assetId = "asset_res_" + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
      activeAssets.set(assetId, {
        assetId,
        realUrl: stableUrl,
        realThumbnailUrl: "",
        metadata: { originalUrl: result.url },
        token: acc.token,
        createdAt: Date.now()
      });
      saveAssetsToDisk();
      maskedUrl = `/api/assets/view/${assetId}`;
    }

    if (result.status === "COMPLETED" && result.url) {
      Vouchers.incrementUsage(req.voucher.code, batchId);
    }

    res.json({
      found: Boolean(result.url),
      url: maskedUrl,
      duration: result.duration || null,
      status: result.status,
      error: result.error || null,
      recipeId: finalRecipeId,
      batchId,
      raw: result.raw,
    });
  } catch (err) {
    console.error("Poll error:", err);
    res.status(500).json({ error: err.message });
  }
}

app.get("/api/poll/:jobId", requireVoucher, handlePoll);
app.get("/api/poll/:accountId/:batchId", requireVoucher, handlePoll);

async function cancelWeavyBatch(jobId) {
  const job = activeJobs.get(jobId);
  if (!job) return { ok: false, error: "Job not found" };

  const acc = Accounts.getAccount(job.accountId);
  if (!acc) return { ok: false, error: "Account not found" };

  const { recipeId, batchId } = job;
  if (!recipeId || !batchId) return { ok: false, error: "Recipe or Batch ID missing" };

  console.log(`[BgPoll] Attempting to cancel batch ${batchId} on Weavy AI...`);
  try {
    const pathCancel = `/api/v1/batches/recipes/${recipeId}/batches/${batchId}/cancel`;
    const resCancel = await weavyFetch(pathCancel, { method: "POST", token: acc.token });
    console.log(`[BgPoll] Cancel request POST status: ${resCancel.status}`);

    const pathDelete = `/api/v1/batches/recipes/${recipeId}/batches/${batchId}`;
    const resDelete = await weavyFetch(pathDelete, { method: "DELETE", token: acc.token });
    console.log(`[BgPoll] Cancel request DELETE status: ${resDelete.status}`);

    return { ok: true };
  } catch (err) {
    console.error("[BgPoll] Failed to send cancel request to Weavy:", err.message);
    return { ok: false, error: err.message };
  }
}

async function handleCancel(req, res) {
  const { jobId } = req.params;
  const job = activeJobs.get(jobId);
  if (!job) {
    return res.status(404).json({ error: "Job tidak ditemukan." });
  }

  if (job.voucherCode !== req.voucher.code) {
    return res.status(403).json({ error: "Akses Ditolak: Anda tidak diizinkan membatalkan job ini." });
  }

  console.log(`[Server] User request cancel for job: ${jobId}`);
  
  job.status = "FAILED";
  job.error = "Dibatalkan oleh pengguna.";
  refundJobAccountRun(job);
  saveJobsToDisk();

  cancelWeavyBatch(jobId).catch(err => {
    console.error(`[BgPoll] Error in cancelWeavyBatch for ${jobId}:`, err.message);
  });

  res.json({ ok: true });
}

app.post("/api/cancel/:jobId", requireVoucher, handleCancel);

// Asset Proxy to mask Weavy media storage URLs from F12 / DOM inspection
app.get("/api/assets/view/:assetId", async (req, res) => {
  let asset = activeAssets.get(req.params.assetId);
  
  // Fallback recovery: kalau asset hilang dari memory (e.g. server restart sebelum persist fix),
  // coba cari job yang mereferensi asset ini dan pakai originalUrl dari job
  if (!asset) {
    const assetId = req.params.assetId;
    const maskedUrl = `/api/assets/view/${assetId}`;
    for (const [jobId, job] of activeJobs.entries()) {
      if (job.url === maskedUrl && job.originalUrl) {
        console.log(`[AssetProxy] Recovery: asset ${assetId} not in activeAssets, using job.originalUrl from ${jobId}`);
        // Buat asset sementara dari originalUrl
        const stableUrl = await uploadToTmpfiles(job.originalUrl).catch(() => job.originalUrl);
        asset = {
          assetId,
          realUrl: stableUrl,
          realThumbnailUrl: "",
          metadata: { originalUrl: job.originalUrl },
          token: "",
          createdAt: Date.now()
        };
        activeAssets.set(assetId, asset);
        saveAssetsToDisk();
        break;
      }
    }
  }
  
  if (!asset) {
    return res.status(404).send("Asset not found or expired");
  }

  let realUrl = asset.realUrl;
  if (req.query.thumbnail === "true" && asset.realThumbnailUrl) {
    realUrl = asset.realThumbnailUrl;
  }

  if (!realUrl) {
    return res.status(404).send("Real URL not found");
  }

  try {
    // Forward Range header if requested by client to support high-performance video streaming/seeking
    const headers = {};
    if (req.headers.range) {
      headers.Range = req.headers.range;
    }

    let response = await fetch(realUrl, { headers });
    // Fallback: kalau tmpfiles.org gagal, coba pakai URL original Weavy
    if (!response.ok && response.status !== 206 && asset.metadata?.originalUrl) {
      console.warn(`[AssetProxy] realUrl failed (${response.status}), trying originalUrl...`);
      const fallbackHeaders = req.headers.range ? { Range: req.headers.range } : {};
      response = await fetch(asset.metadata.originalUrl, { headers: fallbackHeaders });
    }
    if (!response.ok && response.status !== 206) {
      return res.status(response.status).send("Failed to fetch asset from provider");
    }

    res.status(response.status);

    const contentType = response.headers.get("content-type") || "";
    const isMedia = contentType.startsWith("video/") || contentType.startsWith("image/");
    if (isMedia) {
      // Cache media for 24 hours in browser — reduces bandwidth & improves playback
      res.setHeader("Cache-Control", "public, max-age=86400, immutable");
    }

    const keys = ["content-type", "content-length", "content-range", "accept-ranges"];
    keys.forEach((k) => {
      const val = response.headers.get(k);
      if (val) res.setHeader(k, val);
    });

    if (!response.headers.get("content-type")) {
      res.setHeader("Content-Type", "video/mp4");
    }

    // Custom download filename — timestamp for uniqueness
    const dlFilename = `Kling-Motion-TEKNO-${Date.now()}.mp4`;
    res.setHeader("Content-Disposition", `inline; filename="${dlFilename}"`);

    if (response.body) {
      Readable.fromWeb(response.body).pipe(res);
    } else {
      res.sendStatus(500);
    }
  } catch (err) {
    console.error("Asset proxy error:", err);
    res.status(500).send("Proxy error");
  }
});

// ═══════════════════════════════════════════════════════
// 💰 FINANCIAL MANAGEMENT — Owner only
// ═══════════════════════════════════════════════════════

// Get financial summary dashboard
app.get("/api/admin/finance/summary", requireOwner, (_req, res) => {
  try {
    const summary = Finance.getSummary();
    const months = Finance.getAvailableMonths();
    res.json({ ok: true, ...summary, availableMonths: months });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Get transactions with filters
app.get("/api/admin/finance/transactions", requireOwner, (req, res) => {
  try {
    const filters = {};
    if (req.query.type) filters.type = req.query.type;
    if (req.query.actor) filters.actor = req.query.actor;
    if (req.query.reseller) filters.relatedResellerId = req.query.reseller;
    if (req.query.category) filters.category = req.query.category;
    if (req.query.day) filters.day = req.query.day;
    if (req.query.week) filters.week = req.query.week;
    if (req.query.month) filters.month = req.query.month;
    if (req.query.from) filters.from = req.query.from;
    if (req.query.to) filters.to = req.query.to;
    if (req.query.search) filters.search = req.query.search;
    if (req.query.limit) filters.limit = parseInt(req.query.limit) || 500;
    const result = Finance.getTransactions(filters);
    res.json({ ok: true, ...result });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Daily report
app.get("/api/admin/finance/report/daily", requireOwner, (req, res) => {
  try {
    const result = Finance.getDailyReport(req.query.date);
    res.json({ ok: true, ...result });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Weekly report
app.get("/api/admin/finance/report/weekly", requireOwner, (req, res) => {
  try {
    const result = Finance.getWeeklyReport(req.query.week);
    res.json({ ok: true, ...result });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Monthly report
app.get("/api/admin/finance/report/monthly", requireOwner, (req, res) => {
  try {
    const result = Finance.getMonthlyReport(req.query.month);
    res.json({ ok: true, ...result });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Reseller-specific financial summary
app.get("/api/admin/finance/reseller/:id", requireOwner, (req, res) => {
  try {
    const result = Finance.getResellerSummary(req.params.id);
    res.json({ ok: true, ...result });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Owner topup reseller balance (with transaction log)
app.post("/api/admin/finance/topup", requireOwner, (req, res) => {
  try {
    const { resellerId, amount, note } = req.body;
    if (!resellerId || !amount || amount <= 0) {
      return res.status(400).json({ error: "resellerId dan amount (positif) wajib diisi." });
    }

    // Update reseller balance
    const settings = OwnerSettings.getSettings();
    const idx = settings.resellers.findIndex((r) => r.id === resellerId);
    if (idx < 0) return res.status(404).json({ error: "Reseller tidak ditemukan." });

    const r = settings.resellers[idx];
    r.balance = (r.balance || 0) + amount;
    OwnerSettings.writeSettings(settings);

    // Log transaction
    const desc = note
      ? `Topup saldo ${r.name}: ${note}`
      : `Topup saldo ${r.name} sebesar ${Finance.formatCurrency(amount)}`;
    Finance.addTransaction({
      type: "topup",
      actor: "owner",
      actorName: "Owner",
      amount: amount,
      description: desc,
      relatedResellerId: resellerId,
      meta: { resellerName: r.name, note: note || "" },
    });

    console.log(`[Finance] Topup: ${r.name} +${Finance.formatCurrency(amount)}, balance now ${Finance.formatCurrency(r.balance)}`);
    res.json({
      ok: true,
      balance: r.balance,
      totalCommission: r.totalCommission,
      msg: `Berhasil topup ${Finance.formatCurrency(amount)} ke ${r.name}. Saldo sekarang: ${Finance.formatCurrency(r.balance)}`,
    });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Activity log (combined)
app.get("/api/admin/finance/logs", requireOwner, (req, res) => {
  try {
    const filters = {};
    if (req.query.limit) filters.limit = parseInt(req.query.limit) || 200;
    if (req.query.type) filters.type = req.query.type;
    if (req.query.search) filters.search = req.query.search;
    const result = Finance.getLogs(filters);
    res.json({ ok: true, ...result });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Health
app.get("/api/health", (_req, res) => res.json({ ok: true }));

// Sync manually with remote server
app.post("/api/admin/accounts/sync", requireAdmin, async (req, res) => {
  if (req.adminRole === "mitra" && !req.resellerInfo.allowPool) {
    return res.status(403).json({ error: "Akses Ditolak: Anda tidak diizinkan menyinkronkan pool akun." });
  }
  try {
    const result = await Accounts.syncWithRemoteServer();
    if (result.ok) {
      res.json({ ok: true, msg: result.msg });
    } else {
      res.status(500).json({ error: result.error });
    }
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// ─── Recipe Migration (Kling 2.6 Standard) ───────────────────────────────────
// POST /api/admin/recipes/migrate
// Body: { activeOnly?: boolean, accountIds?: string[], delayMs?: number }
//   activeOnly   → only non-archived accounts (default: false)
//   accountIds   → only these specific account IDs (overrides activeOnly)
//   delayMs      → ms between API calls (default: 1200)
app.post("/api/admin/recipes/migrate", requireAdmin, async (req, res) => {
  // Mitras are not allowed to mass-migrate recipes
  if (req.adminRole === "mitra") {
    return res.status(403).json({ error: "Akses Ditolak: Migrasi recipe khusus owner." });
  }

  const { activeOnly = false, accountIds = null, delayMs = 1200 } = req.body || {};

  try {
    const all = Accounts.listAccounts();
    let targets = activeOnly ? all.filter((a) => !a.archived) : all;
    if (Array.isArray(accountIds) && accountIds.length) {
      targets = targets.filter((a) => accountIds.includes(a.id));
    }

    // Send immediate response (this is a long-running op, runs in background)
    res.json({
      ok: true,
      msg: `Memulai migrasi ${targets.length} akun ke Kling 2.6 Standard...`,
      total: targets.length,
    });

    const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
    let updated = 0, recreated = 0, failed = 0, skipped = 0;
    const results = [];

    for (let i = 0; i < targets.length; i++) {
      const acc = targets[i];
      const tag = `[${i + 1}/${targets.length}] ${acc.label} (${acc.id})`;
      console.log(`[Migrate] ${tag} → recipeId ${acc.recipeId || "(none)"}`);

      if (!acc.token || !acc.recipeId) {
        console.log(`[Migrate] ${tag} ⏭️  skipped (missing token/recipeId)`);
        skipped++;
        results.push({ accountId: acc.id, label: acc.label, status: "skipped" });
        continue;
      }

      try {
        const newRecipeId = await ensureKling26Recipe(acc.token, acc.recipeId, (msg) => {
          console.log(`[Migrate]   ${msg}`);
        });
        if (!newRecipeId) {
          failed++;
          results.push({ accountId: acc.id, label: acc.label, status: "failed" });
        } else if (newRecipeId === acc.recipeId) {
          updated++;
          results.push({ accountId: acc.id, label: acc.label, status: "updated", recipeId: newRecipeId });
        } else {
          Accounts.updateAccount(acc.id, { recipeId: newRecipeId });
          recreated++;
          results.push({ accountId: acc.id, label: acc.label, status: "recreated", oldRecipeId: acc.recipeId, newRecipeId });
        }
      } catch (err) {
        failed++;
        results.push({ accountId: acc.id, label: acc.label, status: "error", error: err.message });
        console.log(`[Migrate] ${tag} ❌ ${err.message}`);
      }

      if (i < targets.length - 1) await sleep(delayMs);
    }

    console.log(`[Migrate] ✅ Done — updated: ${updated}, recreated: ${recreated}, failed: ${failed}, skipped: ${skipped}`);
  } catch (err) {
    console.error("[Migrate] Fatal:", err);
    if (!res.headersSent) res.status(500).json({ error: err.message });
  }
});

// GET /api/admin/recipes/migration-log → lihat hasil migrasi terakhir
app.get("/api/admin/recipes/migration-log", requireAdmin, async (_req, res) => {
  try {
    const logPath = path.join(__dirname, "data/migration-log.json");
    if (!fs.existsSync(logPath)) {
      return res.json({ ok: true, exists: false, msg: "Belum pernah ada migrasi." });
    }
    const data = JSON.parse(fs.readFileSync(logPath, "utf-8"));
    res.json({ ok: true, exists: true, ...data });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// ─── Pool Account Settings (Admin) ──────────────────────────────────────────

// GET /api/admin/pool/settings
app.get("/api/admin/pool/settings", requireOwner, (_req, res) => {
  try {
    res.json({ ok: true, ...Accounts.getPoolSettings() });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// PUT /api/admin/pool/settings
app.put("/api/admin/pool/settings", requireOwner, (req, res) => {
  try {
    Accounts.updatePoolSettings(req.body);
    res.json({ ok: true, ...Accounts.getPoolSettings() });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// PUT /api/admin/accounts/:id/max-runs — set per-account max runs
app.put("/api/admin/accounts/:id/max-runs", requireOwner, (req, res) => {
  try {
    const { maxRuns } = req.body;
    const updated = Accounts.setAccountMaxRuns(req.params.id, maxRuns);
    if (!updated) return res.status(404).json({ error: "Account not found" });
    res.json({ ok: true, id: updated.id, maxRuns: updated.maxRuns || Accounts.getPoolSettings().defaultMaxRuns });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// POST /api/admin/pool/reset-monthly — trigger monthly reset manually
app.post("/api/admin/pool/reset-monthly", requireOwner, (_req, res) => {
  try {
    const result = Accounts.resetMonthlyLimits();
    console.log(`[Pool] Monthly reset triggered by admin — ${result.reset} accounts reset (${result.month})`);
    res.json({ ok: true, ...result });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// ─── Auto-Token Bot API ──────────────────────────────────────────────────────

// SSE stream: real-time log from bot
app.get("/api/admin/auto-token/stream", requireBotAccess, (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.flushHeaders();
  
  // Tag response with admin role for log filtering
  res.adminRole = req.adminRole;

  // Send ping to confirm connection
  res.write(`data: ${JSON.stringify({ type: "connected", msg: "🔌 Log stream terhubung" })}\n\n`);
  addSseClient(res);
});

// Manual trigger: run one full auto-token flow
app.post("/api/admin/auto-token/run", requireBotAccess, async (req, res) => {
  const { force = true, mode = "all" } = req.body || {};
  // Don't await — run in background, stream logs via SSE
  triggerAutoToken(force, { mode }).catch(() => {});
  res.json({ ok: true, msg: `Bot (${mode}) dimulai. Pantau log via SSE stream.` });
});

// Single account creation: Google Sign-Up + Weavy Login (1 akun baru)
app.post("/api/admin/auto-token/create-account", requireBotAccess, async (req, res) => {
  try {
    const { email } = req.body || {};
    if (!email || typeof email !== "string" || !email.includes("@")) {
      return res.status(400).json({ error: "Email wajib diisi (string valid)." });
    }

    broadcastLog({ type: "start", msg: `🆕 Memulai pembuatan akun Figma: ${email}` });

    // Run in background — don't await so response returns immediately
    signUpFigmaAndLoginWeavy(email.trim(), broadcastLog)
      .then(async ({ token }) => {
        broadcastLog(`📋 Memastikan recipe Kling 2.6...`, "info");
        const recipeId = await ensureKling26Recipe(token, null, broadcastLog);

        broadcastLog(`💾 Menyimpan akun ke pool: ${email}...`, "info");
        const label = email.trim().split("@")[0];
        const acc = Accounts.addAccount({ token, recipeId, label, email: email.trim() });

        broadcastLog({ type: "success", msg: `✅ Akun baru berhasil dibuat & disimpan: ${label} (${acc.id})` });
      })
      .catch((err) => {
        broadcastLog({ type: "error", msg: `❌ Gagal membuat akun ${email}: ${err.message}` });
      });

    res.json({ ok: true, msg: `Pembuatan akun Google "${email}" dimulai. Pantau log via SSE stream.` });
  } catch (err) {
    broadcastLog({ type: "error", msg: `❌ Gagal memulai: ${err.message}` });
    res.status(500).json({ error: err.message });
  }
});
app.post("/api/admin/auto-token/start-bulk", requireBotAccess, async (req, res) => {
  try {
    const { accounts, emails } = req.body || {};

    // Support format lama (emails array) → konversi ke accounts
    let accountList = accounts;
    if (!accountList && emails && Array.isArray(emails)) {
      accountList = emails.map(e => ({ email: (typeof e === "string" ? e : e.email || "").trim(), password: "" }));
    }

    if (!accountList || !Array.isArray(accountList) || !accountList.length) {
      return res.status(400).json({ error: "accounts[] wajib diisi (array of {email, password})." });
    }

    // Filter & bersihkan
    accountList = accountList.filter(a => a.email && a.email.includes("@")).map(a => ({
      email: a.email.trim(),
      password: (a.password || "").trim(),
    }));

    if (!accountList.length) {
      return res.status(400).json({ error: "Tidak ada akun valid (harus mengandung @)." });
    }

    // Jalankan di background — jangan await agar response langsung kembali
    runBulkFlow(accountList, broadcastLog)
      .then((result) => {
        broadcastLog(`📊 Bulk selesai: ${result.success} berhasil, ${result.failed} gagal.`, result.ok ? "success" : "warn");
      })
      .catch((err) => {
        broadcastLog(`❌ Bulk error: ${err.message}`, "error");
      });
    res.json({ ok: true, msg: `Bulk bot dimulai untuk ${accountList.length} akun Figma.` });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Manual stop: force abort the running bot
app.post("/api/admin/auto-token/stop", requireBotAccess, async (req, res) => {
  try {
    // Tulis false ke .env biar gak auto-start waktu restart
    process.env.AUTO_TOKEN_ENABLED = "false";
    process.env.AUTO_REVIVE_ENABLED = "false";
    updateEnvValue("AUTO_TOKEN_ENABLED", "false");
    updateEnvValue("AUTO_REVIVE_ENABLED", "false");
    const result = await stopSchedulerBot();
    res.json(result);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Status of scheduler + last run
app.get("/api/admin/auto-token/status", requireBotAccess, (_req, res) => {
  res.json(getSchedulerStatus());
});

function updateEnvValue(key, value) {
  const envPath = path.join(__dirname, ".env");
  let content = "";
  try {
    content = fs.readFileSync(envPath, "utf8");
  } catch {
    content = "";
  }

  const regex = new RegExp(`^${key}=.*$`, "m");
  if (regex.test(content)) {
    content = content.replace(regex, `${key}=${value}`);
  } else {
    content += `${content.endsWith("\n") || !content ? "" : "\n"}${key}=${value}\n`;
  }

  fs.writeFileSync(envPath, content, "utf8");
}

// Get/update scheduler config
app.get("/api/admin/auto-token/config", requireBotAccess, (_req, res) => {
  const status = getSchedulerStatus();
  res.json({
    creatorEnabled: status.creatorActive,
    creatorIntervalMinutes: status.creatorIntervalMinutes || parseInt(process.env.AUTO_TOKEN_INTERVAL_MINUTES || "45", 10),
    reviveEnabled: status.reviveActive,
    reviveIntervalMinutes: status.reviveIntervalMinutes || parseInt(process.env.AUTO_REVIVE_INTERVAL_MINUTES || "15", 10),
    headless: process.env.PLAYWRIGHT_HEADLESS !== "false",
  });
});

app.post("/api/admin/auto-token/config", requireBotAccess, (req, res) => {
  const { creatorEnabled, creatorIntervalMinutes, reviveEnabled, reviveIntervalMinutes, headless } = req.body || {};
  let msgParts = [];
  const currentStatus = getSchedulerStatus();

  // 1. Creator Interval
  if (creatorIntervalMinutes !== undefined) {
    const parsed = Number(creatorIntervalMinutes);
    if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1440) {
      return res.status(400).json({ error: "Interval scheduler Pembuat Akun harus antara 1 sampai 1440 menit." });
    }
    const val = Math.round(parsed);
    process.env.AUTO_TOKEN_INTERVAL_MINUTES = String(val);
    try {
      updateEnvValue("AUTO_TOKEN_INTERVAL_MINUTES", val);
      msgParts.push(`Interval Pembuat Akun: ${val} menit`);
    } catch (e) {
      return res.status(500).json({ error: "Gagal menyimpan interval Pembuat Akun ke .env" });
    }
  }

  // 2. Revive Interval
  if (reviveIntervalMinutes !== undefined) {
    const parsed = Number(reviveIntervalMinutes);
    if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1440) {
      return res.status(400).json({ error: "Interval scheduler Pemulih Akun harus antara 1 sampai 1440 menit." });
    }
    const val = Math.round(parsed);
    process.env.AUTO_REVIVE_INTERVAL_MINUTES = String(val);
    try {
      updateEnvValue("AUTO_REVIVE_INTERVAL_MINUTES", val);
      msgParts.push(`Interval Pemulih Akun: ${val} menit`);
    } catch (e) {
      return res.status(500).json({ error: "Gagal menyimpan interval Pemulih Akun ke .env" });
    }
  }

  // 3. Headless Toggle
  if (headless !== undefined) {
    const headlessStr = headless ? "true" : "false";
    process.env.PLAYWRIGHT_HEADLESS = headlessStr;
    try {
      updateEnvValue("PLAYWRIGHT_HEADLESS", headlessStr);
      msgParts.push(`Browser: ${headless ? "Sembunyikan" : "Tampilkan"}`);
    } catch (e) {}
  }

  // 4. Creator Active Toggle
  if (creatorEnabled === true) {
    process.env.AUTO_TOKEN_ENABLED = "true";
    try { updateEnvValue("AUTO_TOKEN_ENABLED", "true"); } catch (e) {}
    const interval = parseInt(process.env.AUTO_TOKEN_INTERVAL_MINUTES || "45", 10);
    startCreatorScheduler(interval);
    msgParts.push("Scheduler Pembuat Akun aktif");
  } else if (creatorEnabled === false) {
    process.env.AUTO_TOKEN_ENABLED = "false";
    try { updateEnvValue("AUTO_TOKEN_ENABLED", "false"); } catch (e) {}
    stopCreatorScheduler();
    msgParts.push("Scheduler Pembuat Akun nonaktif");
  } else if (creatorIntervalMinutes !== undefined && currentStatus.creatorActive) {
    const interval = parseInt(process.env.AUTO_TOKEN_INTERVAL_MINUTES || "45", 10);
    startCreatorScheduler(interval);
    msgParts.push("Scheduler Pembuat Akun dijalankan ulang");
  }

  // 5. Revive Active Toggle
  if (reviveEnabled === true) {
    process.env.AUTO_REVIVE_ENABLED = "true";
    try { updateEnvValue("AUTO_REVIVE_ENABLED", "true"); } catch (e) {}
    const interval = parseInt(process.env.AUTO_REVIVE_INTERVAL_MINUTES || "15", 10);
    startReviveScheduler(interval);
    msgParts.push("Scheduler Pemulih Akun aktif");
  } else if (reviveEnabled === false) {
    process.env.AUTO_REVIVE_ENABLED = "false";
    try { updateEnvValue("AUTO_REVIVE_ENABLED", "false"); } catch (e) {}
    stopReviveScheduler();
    msgParts.push("Scheduler Pemulih Akun nonaktif");
  } else if (reviveIntervalMinutes !== undefined && currentStatus.reviveActive) {
    const interval = parseInt(process.env.AUTO_REVIVE_INTERVAL_MINUTES || "15", 10);
    startReviveScheduler(interval);
    msgParts.push("Scheduler Pemulih Akun dijalankan ulang");
  }

  if (msgParts.length === 0) {
    return res.status(400).json({ error: "No configuration options provided" });
  }

  res.json({ ok: true, msg: msgParts.join(", ") });
});

app.post("/api/admin/auto-token/test", requireBotAccess, (req, res) => {
  try {
    const { delaySeconds = 15 } = req.body || {};
    const result = scheduleTestRun(delaySeconds);
    res.json({
      ok: true,
      msg: `Test scheduler dijadwalkan ${result.delaySeconds} detik dari sekarang.`,
      runAt: result.runAt,
    });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// ─── ICLOUD HME API Proxy ────────────────────────────────────────────────────

// Generate email @tekno-mail.my.id via ICLOUD HME
// Generate email @tekno-mail.my.id via local ICLOUD (accounts.json)
app.post("/api/admin/icloud/generate", requireAdmin, async (req, res) => {
  try {
    const { prefix, count = 1 } = req.body || {};
    const accounts = IcloudLocal.getAccounts();
    if (!accounts.length) {
      return res.status(500).json({ error: "Tidak ada akun iCloud di accounts.json" });
    }
    const acc = accounts[0];

    const emails = [];
    const limit = Math.min(Math.max(1, count), 10);
    for (let i = 0; i < limit; i++) {
      try {
        const clean = (prefix || "").toLowerCase().replace(/[^a-z0-9]/g, "");
        const num = String(Math.floor(Math.random() * 1000)).padStart(3, "0");
        const base = clean || `user${Date.now().toString(36)}${i}`;
        const email = `${base}${num}@tekno-mail.my.id`;

        // Track locally
        IcloudLocal.addTrackedEmail("admin", {
          accountId: acc.id,
          hme: email,
          anonymousId: "gen_" + crypto.randomUUID().slice(0, 8),
          label: clean || "",
          note: "Admin Dashboard",
        });
        emails.push(email);
      } catch (e) {
        console.error("[ICLOUD generate]", e.message);
      }
    }

    res.json({ ok: true, emails, count: emails.length });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// List tracked emails dari local ICLOUD (tracked_emails.json)
app.get("/api/admin/icloud/emails", requireAdmin, async (_req, res) => {
  try {
    const emails = IcloudLocal.getTrackedEmails();

    // Map ke format simpel
    const list = emails.map((e) => ({
      email: e.hme || "",
      createdAt: e.createdAt || "",
      expiresAt: e.expiresAt || null,
      pinned: e.pinned || false,
    }));

    res.json({ ok: true, emails: list });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Cek status koneksi iCloud — test via local accounts.json (Apple mailws2)
app.get("/api/admin/icloud/test-connection", requireAdmin, async (_req, res) => {
  try {
    const accounts = IcloudLocal.getAccounts();
    if (!accounts.length) {
      return res.json({ ok: false, error: "Tidak ada akun iCloud terdaftar di accounts.json" });
    }
    const acc = accounts[0];
    const testBody = {
      responseType: "THREAD_DIGEST",
      includeFolderStatus: true,
      maxResults: 1,
      sessionHeaders: { folder: "INBOX", threadmode: 1 },
    };
    const sub = acc.subdomain || "p188-maildomainws.icloud.com";
    const parts = sub.split("-", 2);
    const prefix = parts[0] || "p149";
    const url = `https://${prefix}-mccgateway.icloud.com/mailws2/v1/thread/search?clientBuildNumber=2618Build25&clientMasteringNumber=2618Build25&clientId=${
      encodeURIComponent(acc.clientId)
    }&dsid=${encodeURIComponent(acc.dsid)}`;
    const resp = await fetch(url, {
      method: "POST",
      headers: {
        accept: "application/json",
        "content-type": "application/json",
        cookie: acc.cookies || "",
        origin: "https://www.icloud.com",
        referer: "https://www.icloud.com/",
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/148.0.0.0 Safari/537.36",
      },
      body: JSON.stringify(testBody),
    });
    const data = await resp.json().catch(() => ({}));
    if (resp.status < 400 && data.threadList) {
      return res.json({ ok: true });
    }
    return res.json({ ok: false, error: `HTTP ${resp.status} — Apple session mungkin expired` });
  } catch (err) {
    return res.json({ ok: false, error: err.message });
  }
});

// Cari kode verifikasi Google di iCloud inbox via local accounts.json (mailws2)
app.post("/api/admin/icloud/find-code", requireAdmin, async (req, res) => {
  try {
    const { email, maxAttempts = 25, skipThreadIds = [] } = req.body || {};
    if (!email) return res.status(400).json({ error: "Parameter 'email' wajib diisi" });

    const accounts = IcloudLocal.getAccounts();
    if (!accounts.length) return res.status(500).json({ error: "Tidak ada akun iCloud di accounts.json" });
    const acc = accounts[0];
    const skipIds = new Set(Array.isArray(skipThreadIds) ? skipThreadIds : []);

    // Helper: ekstrak kode verifikasi dari teks
    function extractVerificationCode(text) {
      if (!text) return null;

      // Helper: deteksi false positive — angka repeating / pattern
      function isFalsePositive(code) {
        if (!code) return true;
        // Skip kode2 obvious dummy
        const blacklist = [
          "000000", "111111", "222222", "333333", "444444", "555555",
          "666666", "777777", "888888", "999999", "123456", "654321",
        ];
        if (blacklist.includes(code)) return true;
        // Skip repeating digit-pair patterns: 232323, 121212, 343434, 565656, 787878, 898989, 909090, 010101
        if (/^(\d{2})\1\1$/.test(code)) return true;
        // Skip sequential repeating: 123123, 456456, 789789
        if (code.length === 6 && code.substring(0, 3) === code.substring(3, 6)) return true;
        return false;
      }

      // Strip HTML tags untuk ekstraksi bersih
      const cleanText = text.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();

      // Prioritas: Figma / Google specific patterns
      const patterns = [
        // Figma "Authentication code" — paling spesifik dulu
        /Authentication\s+code\s*[:：]?\s*(\d{6,7})\b/i,
        /Figma\s+(?:authentication|verification)\s+code\s*[:：]?\s*(\d{6,7})\b/i,
        /(?:authentication|verification)\s+code\s*(?:is)?\s*[:：]?\s*(\d{6,7})\b/i,
        // Figma OTP — teks biasa tanpa label khusus
        /(?:enter|type|input)\s+(?:the\s+)?(?:code|otp)\s*[:：]?\s*(\d{6,7})\b/i,
        /code\s+to\s+(?:log\s*in|sign\s*in|verify|authenticate)\s*[:：]?\s*(\d{6,7})\b/i,
        // Google-specific
        /G[-\s]*(\d{6})\b/i,
        /(\d{6})\s+is\s+(?:your|the)\s+(?:Google|verification)\s+code/i,
        /(?:Google|verification)\s+code\s+(?:is\s+)?(\d{6,7})\b/i,
        // Generic OTP patterns
        /one.?time\s+(?:verification\s+)?(?:pass)?code\s*(?:is)?\s*[:：]\s*(\d{6,7})\b/i,
        /verification\s+code\s*(?:is)?\s*[:：]\s*(\d{6,7})\b/i,
        /code\s*(?:is)?\s*[:：]\s*(\d{6,7})\b/i,
        /security\s+code\s*[:：]?\s*(\d{6,7})\b/i,
        /your\s+code\s*(?:is)?\s*[:：]?\s*(\d{6,7})\b/i,
      ];
      for (const p of patterns) {
        const m = cleanText.match(p);
        if (m && m[1]) {
          const upper = m[1].toUpperCase();
          const wordBlacklist = ["GOOGLE","VERIFY","CLOUD","PLEASE","SECURE","ACCESS","SIGNIN","FIGMA"];
          if (wordBlacklist.includes(upper)) continue;
          if (isFalsePositive(upper)) continue;
          return upper;
        }
      }

      // Fallback 1: 6-7 digit di dekat kata kunci verifikasi (dalam clean text)
      const keywords = /(?:authentication|verification|one.?time|security|figma|google)\s*(?:code|otp)?/i;
      const idx = cleanText.toLowerCase().search(keywords);
      if (idx >= 0) {
        const snippet = cleanText.substring(Math.max(0, idx - 30), idx + 400);
        // Cari SEMUA kandidat 6-7 digit, ambil yang TERAKHIR (biasanya kode asli)
        const allDigits = [...snippet.matchAll(/\b(\d{6,7})\b/g)];
        for (let i = allDigits.length - 1; i >= 0; i--) {
          const candidate = allDigits[i][1];
          if (!isFalsePositive(candidate)) return candidate;
        }
      }

      // Fallback 2: cari 6-7 digit di SELURUH teks — tapi ambil yang TERAKHIR
      //            (kode asli biasanya di akhir email body)
      const allGlobalDigits = [...cleanText.matchAll(/\b(\d{6,7})\b/g)];
      for (let i = allGlobalDigits.length - 1; i >= 0; i--) {
        const candidate = allGlobalDigits[i][1];
        if (!isFalsePositive(candidate)) return candidate;
      }

      return null;
    }

    // Gunakan fungsi mailws2 langsung untuk polling
    // Gunakan fungsi mailws2 langsung untuk polling
    async function callMailws2(endpoint, body) {
      const sub = (acc.subdomain && acc.subdomain.includes("icloud.com")) 
        ? acc.subdomain 
        : "p188-maildomainws.icloud.com";
      const parts = sub.split("-", 2);
      const prefix = parts[0] || "p149";
      const sep = endpoint.includes("?") ? "&" : "?";
      const url = `https://${prefix}-mccgateway.icloud.com/mailws2/v1/${endpoint}${sep}clientBuildNumber=2618Build25&clientMasteringNumber=2618Build25&clientId=${
        encodeURIComponent(acc.clientId)
      }&dsid=${encodeURIComponent(acc.dsid)}`;
      
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 8000);

      try {
        const resp = await fetch(url, {
          method: "POST",
          headers: {
            accept: "application/json",
            "content-type": "application/json",
            cookie: acc.cookies || "",
            origin: "https://www.icloud.com",
            referer: "https://www.icloud.com/",
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/148.0.0.0 Safari/537.36",
          },
          body: JSON.stringify(body),
          signal: controller.signal,
        });
        const text = await resp.text();
        clearTimeout(timeoutId);
        let data;
        try { data = JSON.parse(text); } catch { throw new Error(`Non-JSON (HTTP ${resp.status})`); }
        return { status: resp.status, data };
      } catch (err) {
        clearTimeout(timeoutId);
        throw err;
      }
    }

    // Polling inbox
    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      await new Promise((r) => setTimeout(r, 3000));

      try {
        // 1. GET thread list
        const threadRes = await callMailws2("thread/search", {
          responseType: "THREAD_DIGEST",
          includeFolderStatus: true,
          maxResults: 100,
          sessionHeaders: { folder: "INBOX", threadmode: 1 },
        });

        if (threadRes.status >= 400 || !threadRes.data?.threadList) {
          if (threadRes.data?.error || threadRes.data?.message) {
            return res.status(401).json({ error: `Apple session invalid: ${threadRes.data.error || threadRes.data.message}` });
          }
          continue;
        }

        const threads = (threadRes.data.threadList || []).map(t => ({
          id: t.threadId || "",
          date: t.timestamp ? new Date(t.timestamp).toISOString() : "",
          timestamp: t.timestamp || 0,
          subject: t.subject || "(Tanpa Subjek)",
          from: t.senders?.length ? t.senders[0] : "Unknown",
          text: t.preview || "",
        }));

        if (!threads.length) continue;

        // ── SORT DESC: thread TERBARU dulu ──
        threads.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));

        // ── SKIP thread yang udah dicoba atau terlalu lama (lebih dari 5 menit) ──
        const freshThreads = threads.filter(t => {
          if (skipIds.has(t.id)) return false;
          if (t.timestamp) {
            let threadTime = Number(t.timestamp);
            if (threadTime < 10000000000) {
              threadTime = threadTime * 1000;
            }
            const ageMs = Date.now() - threadTime;
            if (ageMs > 300000) return false; // 5 menit
          }
          return true;
        });

        // 2. Cari semua thread yang relevan (newest first)
        const searchStr = (t) => [t.subject, t.from, t.text].join(" ").toLowerCase();
        const emailLower = email.toLowerCase();
        const candidateThreads = freshThreads.filter(t => {
          const s = searchStr(t);
          return s.includes(emailLower) || s.includes("figma") || s.includes("verification") || s.includes("authentication");
        });

        if (!candidateThreads.length) continue;

        // Helper: get folder name from mboxRef
        const getFolder = (m) => {
          let mbox = "";
          if (m.mboxRef) {
            mbox = typeof m.mboxRef === "object" ? String(m.mboxRef.id || "") : String(m.mboxRef);
          }
          const map = { "34": "Sent", "33": "Drafts", "36": "Junk", "32": "Trash", "35": "Archive" };
          return map[mbox] || "INBOX";
        };

        let foundCode = null;
        let matchedThreadId = null;

        for (const thread of candidateThreads) {
          // 3. GET message list untuk thread ini
          const msgRes = await callMailws2("message/list?clientIntent=fetchMessageListByThreadId", {
            domain: "email",
            properties: [
              "uid", "appleThrid", "identifier", "stateInternalDate", "mboxRef", "parts",
              { type: "fieldOf", property: "rfc822HeaderBytes", value: "From", decoder: "encodedWords", alias: "from" },
              { type: "fieldOf", property: "rfc822HeaderBytes", value: "To", decoder: "encodedWords", alias: "to" },
              { type: "fieldOf", property: "rfc822HeaderBytes", value: "Subject", decoder: "encodedWords", alias: "subject" },
            ],
            limit: 10,
            predicate: { expression: { property: "appleThrid", type: "property" }, value: thread.id, type: "eq" },
            orderby: { expressions: [{ property: "stateInternalDate", type: "property" }], ascending: false },
          });

          const msgs = Array.isArray(msgRes.data?.domainObjects) ? msgRes.data.domainObjects : [];
          if (!msgs.length) continue;

          // Fetch body HANYA untuk 3 message TERBARU
          const latest3 = msgs.slice(0, 3);
          
          for (const msg of latest3) {
            const uid = msg.uid || "";
            if (!uid) continue;

            // Cek umur pesan — abaikan jika lebih tua dari 5 menit (300.000 ms)
            const internalDate = msg.stateInternalDate;
            if (internalDate) {
              let msgTime = Number(internalDate);
              if (msgTime < 10000000000) {
                msgTime = msgTime * 1000;
              }
              const ageMs = Date.now() - msgTime;
              if (ageMs > 300000) {
                console.log(`[find-code] Mengabaikan email OTP lama (umur: ${Math.round(ageMs / 1000)} detik) untuk ${email}`);
                continue;
              }
            }

            // Cari partId dari message.parts
            let htmlPartId = "", plainPartId = "";
            if (Array.isArray(msg.parts)) {
              for (const part of msg.parts) {
                const ct = (part.contentType || "").toLowerCase();
                if (ct === "text/html" && !htmlPartId) htmlPartId = part.partId || "";
                else if (ct === "text/plain" && !plainPartId) plainPartId = part.partId || "";
              }
            }

            const folderName = getFolder(msg);
            const partIds = [plainPartId, htmlPartId].filter(Boolean);
            let msgText = "";
            let msgHtml = "";
            for (const partId of partIds) {
              try {
                const getRes = await callMailws2("message/get", {
                  uid,
                  parts: [partId],
                  sessionHeaders: { folder: folderName, threadmode: 1 },
                });
                if (getRes.status === 200 && Array.isArray(getRes.data?.parts)) {
                  const content = getRes.data.parts[0]?.content || "";
                  msgText += content + "\n";
                  if (partId === htmlPartId) msgHtml += content + "\n";
                }
              } catch (_) {}
            }

            const cleanMsg = msgText.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
            const combinedText = (msgText + " " + msgHtml.replace(/<[^>]+>/g, " ")).toLowerCase();
            const toHeader = (msg.to || "").toLowerCase();

            // WAJIB mencocokkan email target di To header atau body pesan
            if (toHeader.includes(emailLower) || combinedText.includes(emailLower)) {
              foundCode = extractVerificationCode(cleanMsg) || extractVerificationCode(combinedText);
              if (foundCode) {
                matchedThreadId = thread.id;
                break;
              }
            }
          }

          if (foundCode) break;
        }

        if (foundCode) {
          return res.json({ ok: true, code: foundCode, email, attempt: attempt + 1, threadId: matchedThreadId });
        }
      } catch (e) {
        console.error(`[find-code] attempt ${attempt + 1}:`, e.message);
      }
    }

    res.json({ ok: false, error: "Kode verifikasi tidak ditemukan", email });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Lihat isi universal inbox via local ICLOUD (Apple mailws2)
app.get("/api/admin/icloud/inbox", requireAdmin, async (_req, res) => {
  try {
    const accounts = IcloudLocal.getAccounts();
    if (!accounts.length) {
      return res.status(500).json({ error: "Tidak ada akun iCloud di accounts.json" });
    }

    const threads = await IcloudLocal._fetchUniversalInbox(accounts[0]);
    res.json({ ok: true, threads: Array.isArray(threads) ? threads.slice(0, 30) : [] });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Thread detail (untuk inbox viewer) via local ICLOUD
app.post("/api/admin/icloud/inbox/thread", requireAdmin, async (req, res) => {
  try {
    const { threadId } = req.body || {};
    if (!threadId) return res.status(400).json({ error: "threadId wajib" });

    const accounts = IcloudLocal.getAccounts();
    if (!accounts.length) {
      return res.status(500).json({ error: "Tidak ada akun iCloud di accounts.json" });
    }

    const messages = await IcloudLocal._fetchUniversalThread(accounts[0], threadId);
    res.json({ ok: true, messages: Array.isArray(messages) ? messages : [] });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// ICLOUD Settings — baca & simpan dari dashboard Admin
app.get("/api/admin/icloud/settings", requireAdmin, (_req, res) => {
  try {
    const cfg = loadIcloudSettings();
    res.json({ ok: true, settings: cfg });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.post("/api/admin/icloud/settings", requireAdmin, (req, res) => {
  try {
    const { apiUrl, sessionToken, deviceId, accountId } = req.body || {};
    const current = loadIcloudSettings();
    if (apiUrl !== undefined) current.apiUrl = String(apiUrl).trim();
    if (sessionToken !== undefined) current.sessionToken = String(sessionToken).trim();
    if (deviceId !== undefined) current.deviceId = String(deviceId).trim();
    if (accountId !== undefined) current.accountId = String(accountId).trim();
    const saved = saveIcloudSettings(current);
    res.json({ ok: true, settings: saved });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// ─── ICLOUD Account Management (local accounts.json) ────────────────────────

// List all iCloud accounts
app.get("/api/admin/icloud/accounts", requireAdmin, (_req, res) => {
  try {
    const accounts = IcloudLocal.getAccounts();
    // Strip cookies from list view (security — only send for single account)
    const safe = accounts.map(a => ({
      id: a.id,
      name: a.name,
      subdomain: a.subdomain,
      dsid: a.dsid,
      clientId: a.clientId,
      email: a.email,
      hasCookies: !!(a.cookies),
      hasAppPassword: !!(a.appPassword),
      createdAt: a.createdAt,
      updatedAt: a.updatedAt,
    }));
    res.json({ ok: true, accounts: safe });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Get single iCloud account (with full credentials)
app.get("/api/admin/icloud/accounts/:id", requireAdmin, (req, res) => {
  try {
    const acc = IcloudLocal.getAccountById(req.params.id);
    if (!acc) return res.status(404).json({ error: "Akun tidak ditemukan" });
    res.json({ ok: true, account: acc });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Add / Update iCloud account
app.post("/api/admin/icloud/accounts", requireAdmin, (req, res) => {
  try {
    const { id, name, subdomain, dsid, clientId, cookies, email, appPassword } = req.body || {};
    if (!dsid || !clientId) {
      return res.status(400).json({ error: "DSID dan Client ID wajib diisi" });
    }
    const acc = IcloudLocal.saveAccount({
      id: id || undefined,
      name: name || "",
      subdomain: subdomain || "p188-maildomainws.icloud.com",
      dsid: String(dsid).trim(),
      clientId: String(clientId).trim(),
      cookies: cookies || "",
      email: email || "",
      appPassword: appPassword || "",
    });
    res.json({ ok: true, account: acc });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Delete iCloud account
app.delete("/api/admin/icloud/accounts/:id", requireAdmin, (req, res) => {
  try {
    const ok = IcloudLocal.deleteAccount(req.params.id);
    if (!ok) return res.status(404).json({ error: "Akun tidak ditemukan" });
    res.json({ ok: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// ─── Start server ────────────────────────────────────────────────────────────

// Prevent server crash from unhandled bot/Playwright errors
process.on("uncaughtException", (err) => {
  console.error("[Server] Uncaught Exception:", err.message);
});
process.on("unhandledRejection", (reason) => {
  console.error("[Server] Unhandled Rejection:", reason?.message || reason);
});

const CREATOR_ENABLED = process.env.AUTO_TOKEN_ENABLED === "true";
const CREATOR_INTERVAL = parseInt(process.env.AUTO_TOKEN_INTERVAL_MINUTES || "45", 10);
const REVIVE_ENABLED = process.env.AUTO_REVIVE_ENABLED === "true";
const REVIVE_INTERVAL = parseInt(process.env.AUTO_REVIVE_INTERVAL_MINUTES || "15", 10);

// ---- Embedded iCloud API (menggantikan icloud.studiolabs.web.id) ----
const icloudRouter = createIcloudRouter(express.Router);
app.use("/api", icloudRouter);

app.listen(PORT, () => {
  console.log(`Kling Motion (Weavy) app running at http://localhost:${PORT}`);
  console.log(`Admin password: ${ADMIN_PASSWORD === "admin123" ? "admin123 (default — ganti via .env ADMIN_PASSWORD)" : "(custom)"}`);
  console.log(`Default recipe: ${DEFAULT_RECIPE_ID}`);

  // Load jobs and resume background polling on startup
  loadJobsFromDisk();
  loadAssetsFromDisk();

  if (CREATOR_ENABLED) {
    startCreatorScheduler(CREATOR_INTERVAL);
    console.log(`🤖 Auto Creator Bot: aktif (interval ${CREATOR_INTERVAL} menit)`);
  } else {
    console.log(`🤖 Auto Creator Bot: nonaktif (set AUTO_TOKEN_ENABLED=true untuk mengaktifkan)`);
  }

  if (REVIVE_ENABLED) {
    startReviveScheduler(REVIVE_INTERVAL);
    console.log(`🤖 Auto Revive Bot: aktif (interval ${REVIVE_INTERVAL} menit)`);
  } else {
    console.log(`🤖 Auto Revive Bot: nonaktif (set AUTO_REVIVE_ENABLED=true untuk mengaktifkan)`);
  }

  // Sync with remote server on startup and set up interval
  if (process.env.REMOTE_SERVER_URL) {
    console.log(`[Sync] Menjalankan sinkronisasi awal dengan ${process.env.REMOTE_SERVER_URL}...`);
    Accounts.syncWithRemoteServer().catch(() => {});
    Vouchers.syncFromRemote().then(r => {
      if (r.ok) console.log(`[Sync] Voucher: ${r.added} baru, ${r.updated} diperbarui (total ${r.total} dari remote)`);
      else console.log(`[Sync] Voucher sync gagal: ${r.error}`);
    }).catch(() => {});

    // Polling every 30 seconds (near-realtime)
    setInterval(() => {
      Accounts.syncWithRemoteServer().catch(() => {});
      Vouchers.syncFromRemote().catch(() => {});
    }, 30 * 1000);
  }
});
