import "dotenv/config";
import express from "express";
import multer from "multer";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";
import { Readable } from "stream";
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, ensureKling26Recipe } 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();

const JOBS_FILE = path.join(__dirname, "data/active_jobs.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);
  }
}

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.";
      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.";
          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") {
        job.status = "FAILED";
        job.error = error || `Task ${status.toLowerCase()} di Weavy.`;
        
        // Refund run counter
        const currentAcc = Accounts.getAccount(job.accountId);
        if (currentAcc && (currentAcc.runs || 0) > 0) {
          Accounts.updateAccount(job.accountId, { runs: currentAcc.runs - 1 });
          console.log(`[Refund] BgPoll failed for ${currentAcc.label}: runs ${currentAcc.runs} -> ${currentAcc.runs - 1}`);
        }
        saveJobsToDisk();
        return;
      }

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

        job.status = "COMPLETED";
        job.progress = 100;
        job.url = `/api/assets/view/${assetId}`;
        job.duration = result.duration || null;

        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).";
    saveJobsToDisk();
  }
}

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 addJob(accountId, batchId, recipeId, voucherCode) {
  const jobId = "job_" + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
  activeJobs.set(jobId, {
    accountId,
    batchId,
    recipeId,
    voucherCode,
    status: "PENDING",
    progress: 5,
    url: null,
    duration: null,
    error: null,
    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}`;

  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();
  }
  for (const [assetId, asset] of activeAssets.entries()) {
    if (now - asset.createdAt > 24 * 60 * 60 * 1000) { // 24 hours TTL
      activeAssets.delete(assetId);
    }
  }
}, 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;
  if (origin && 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, */*",
    "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;
  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, () => {
    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", 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 });
  }
});

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
      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, 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);
      } else {
        return res.status(response.status).json(data);
      }
    } catch (e) {
      return res.status(500).json({ error: `Gagal meneruskan ke remote: ${e.message}` });
    }
  }

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

// ─── 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) < (a.maxRuns || Accounts.DEFAULT_MAX_RUNS) &&
    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) {
      // 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) {
          // Recipe doesn't belong to this account — auto-create one
          console.warn(`Account ${acc.label} got 403 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) {
            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 => 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, req.voucher.code);
        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: ["fal-ai/kling-video/v2.6/pro/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, req.voucher.code);
                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 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." });
    }
    // 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,
    });
  }

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

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

    const response = await fetch(realUrl, { headers });
    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");
    }

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

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

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

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

  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 2 minutes
    setInterval(() => {
      Accounts.syncWithRemoteServer().catch(() => {});
      Vouchers.syncFromRemote().catch(() => {});
    }, 2 * 60 * 1000);
  }
});
