import "dotenv/config";
import express from "express";
import multer from "multer";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";
import { buildExecutePayload } from "./lib/weavy-payload.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 } from "./lib/auto-token.js";
import * as OwnerSettings from "./lib/owner-settings-store.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";

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

function addJob(accountId, batchId, recipeId) {
  const jobId = "job_" + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
  activeJobs.set(jobId, { accountId, batchId, recipeId, createdAt: Date.now() });
  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}`;

  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();
  for (const [jobId, job] of activeJobs.entries()) {
    if (now - job.createdAt > 15 * 60 * 1000) { // 15 minutes TTL
      activeJobs.delete(jobId);
    }
  }
  for (const [assetId, asset] of activeAssets.entries()) {
    if (now - asset.createdAt > 60 * 60 * 1000) { // 1 hour TTL
      activeAssets.delete(assetId);
    }
  }
}, 5 * 60 * 1000);

app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  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, */*",
    "content-type": "application/json",
    "x-weavy-auth-provider": "firebase",
    "x-app-version": "4.1.306",
    authorization: `Bearer ${token}`,
    ...extraHeaders,
  };
  return fetch(`${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;
  res.json({
    ok: true,
    code: req.voucher.code,
    tier: req.voucher.tier,
    dailyLimit: req.voucher.dailyLimit || 3,
    usage: usage,
    expiresAt: req.voucher.expiresAt,
  });
});

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, () => {
    if (req.adminRole !== "owner") {
      return res.status(403).json({ error: "Akses Ditolak: Hanya Owner yang diizinkan." });
    }
    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,
    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 } = req.body || {};
    const reseller = OwnerSettings.addReseller({ name, password, quota, balance, customResellerPrice, customSrp, customResellerVipPrice, customVipSrp, allowBot, allowPool, allowArchive });
    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 });
  }
});

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
    });
  } 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", requireOwner, (_req, res) => {
  res.json({ accounts: Accounts.listAccounts() });
});

app.post("/api/admin/accounts", requireOwner, (req, res) => {
  try {
    const {
      token,
      recipeId,
      label,
      email,
      password,
      runs,
      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,
      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", requireOwner, (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", requireOwner, (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 });
  }
});

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 });
  }
});


// 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, (req, res) => {
  const allVouchers = Vouchers.listVouchers();
  if (req.adminRole === "mitra") {
    const filtered = allVouchers.filter((v) => v.createdBy === req.resellerInfo.resellerId);
    return res.json({ vouchers: filtered });
  }
  res.json({ vouchers: allVouchers });
});

app.post("/api/admin/vouchers", requireAdmin, (req, res) => {
  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
      if ((reseller.balance || 0) < activeResellerPrice) {
        const bal = reseller.balance || 0;
        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 ${bal.toLocaleString('id-ID')}). Silakan hubungi Owner untuk topup.`
        });
      }
 
      // 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
    });

    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
        };
      }
    }
    res.json(responsePayload);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

app.delete("/api/admin/vouchers/:code", requireAdmin, (req, res) => {
  if (req.adminRole === "mitra") {
    const allVouchers = Vouchers.listVouchers();
    const v = allVouchers.find((x) => x.code === req.params.code.trim().toUpperCase());
    if (!v || v.createdBy !== req.resellerInfo.resellerId) {
      return res.status(403).json({ error: "Akses Ditolak: Anda hanya dapat menghapus voucher Anda sendiri." });
    }
  }
  const ok = Vouchers.deleteVoucher(req.params.code);
  if (!ok) return res.status(404).json({ error: "Not found" });
  res.json({ ok: true });
});

app.put("/api/admin/vouchers/:code", requireAdmin, (req, res) => {
  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 });
  }
});

// ─── 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 });
  }
});

async function getOrReviveAccount(excludeIds = null) {
  const tried = excludeIds instanceof Set ? excludeIds : new Set(excludeIds || []);
  
  // 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 &&
    (Accounts.isExpired(a.token) || a.expired) &&
    (a.runs || 0) < Accounts.MONTHLY_RUN_LIMIT &&
    a.email && a.password &&
    !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." });

    const fd = new FormData();
    const blob = new Blob([req.file.buffer], { type: req.file.mimetype });
    fd.append("file", blob, req.file.originalname);
    fd.append("type", req.file.mimetype);

    const r = await fetch(`${WEAVY_API}/api/v1/assets/upload`, {
      method: "POST",
      headers: {
        accept: "application/json, text/plain, */*",
        authorization: `Bearer ${acc.token}`,
        "x-weavy-auth-provider": "firebase",
        "x-app-version": "4.1.246",
      },
      body: fd,
    });

    const data = await readResponse(r);
    if (!r.ok) {
      console.error("Upload failed:", r.status, data.json || data.text);
      return res.status(r.status).json({ error: extractWeavyError(data), accountId: acc.id });
    }
    const maskedData = registerAndMaskUploadResponse(data.json, 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 = "image",
      numberOfRuns = 1,
      excludeIds = [],
    } = 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) {
      const acc = await getOrReviveAccount(triedIds);
      if (!acc) break;

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

      triedIds.add(acc.id);
      attempts++;

      const recipeId = acc.recipeId || DEFAULT_RECIPE_ID;

      // Fetch recipe nodes dynamically to avoid UUID mismatch
      let recipeNodes = null;
      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 });
          lastError = "Authentication Error";
          continue;
        }
        if (recipeRes.status === 403) {
          console.warn(`Account ${acc.label} got 403 on recipe check, skipping...`);
          broadcastLog({
            type: "warn",
            msg: `⚠️ [Generate] Akun ${acc.email || acc.label} kena 403 saat cek resep, skip ke akun lain.`,
          });
          lastError = "Recipe access denied";
          continue;
        }

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

          const modelNode = 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) {
        Accounts.markUsed(acc.id);
        const batchId = data.json?.batchId || data.json?.id || "";
        const jobId = addJob(acc.id, batchId, recipeId);
        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 });
        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/\\rkling-v3-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) {
                Accounts.markUsed(acc.id);
                const batchId = retryData.json?.batchId || retryData.json?.id || "";
                const jobId = addJob(acc.id, batchId, recipeId);
                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);
        }
        continue; // triedIds already has this ID — won't retry same account
      }

      // Other error → return immediately
      console.error("Generate failed:", r.status, data.json || data.text);
      return res.status(r.status).json({ error: extractWeavyError(data), accountId: acc.id, accountUsed: acc.label });
    }

    // 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." });
    }
    accountId = job.accountId;
    batchId = job.batchId;
    recipeId = job.recipeId;
  }

  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...`);
      }

      // Refund the account run counter since the generation failed!
      const currentAcc = Accounts.getAccount(accountId);
      if (currentAcc && (currentAcc.runs || 0) > 0) {
        Accounts.updateAccount(accountId, { 
          runs: currentAcc.runs - 1 
        });
        console.log(`[Refund] Generation failed for ${currentAcc.label || accountId}: runs ${currentAcc.runs} → ${currentAcc.runs - 1}`);
      }
    }

    let maskedUrl = null;
    if (result.url) {
      const assetId = "asset_res_" + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
      activeAssets.set(assetId, {
        assetId,
        realUrl: result.url,
        realThumbnailUrl: "",
        metadata: {},
        token: acc.token,
        createdAt: Date.now()
      });
      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,
      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);

// Asset Proxy to mask Weavy media storage URLs from F12 / DOM inspection
app.get("/api/assets/view/:assetId", async (req, res) => {
  const asset = activeAssets.get(req.params.assetId);
  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 {
    const response = await fetch(realUrl);
    if (!response.ok) {
      return res.status(response.status).send("Failed to fetch asset from provider");
    }

    res.setHeader("Content-Type", response.headers.get("content-type") || "application/octet-stream");
    res.setHeader("Content-Length", response.headers.get("content-length") || "");
    
    const buffer = Buffer.from(await response.arrayBuffer());
    res.send(buffer);
  } catch (err) {
    console.error("Asset proxy error:", err);
    res.status(500).send("Proxy error");
  }
});

// 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 });
  }
});


// ─── 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.` });
});

// Manual stop: force abort the running bot
app.post("/api/admin/auto-token/stop", requireBotAccess, async (req, res) => {
  try {
    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 });
  }
});

// ─── 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);

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}`);

  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(() => {});

    // Polling every 2 minutes
    setInterval(() => {
      Accounts.syncWithRemoteServer().catch(() => {});
    }, 2 * 60 * 1000);
  }
});
