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,
  startScheduler,
  stopScheduler,
  getSchedulerStatus,
  addSseClient,
  stopSchedulerBot,
} from "./lib/auto-scheduler.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";

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");
  if (req.method === "OPTIONS") {
    return res.sendStatus(200);
  }
  next();
});

app.use(express.json({ limit: "10mb" }));
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);
  res.json({ ok: true, tier: val.tier });
});

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

function requireAdmin(req, res, next) {
  const pass = req.headers["x-admin-password"];
  if (pass !== ADMIN_PASSWORD) return res.status(401).json({ error: "Unauthorized" });
  next();
}

// ─── Admin API: account management ──────────────────────────────────────────

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 || {};
  if (password !== ADMIN_PASSWORD) {
    recordAttempt(ip);
    return res.status(401).json({ error: "Password admin salah!" });
  }
  clearAttempts(ip);
  res.json({ ok: true });
});

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

app.get("/api/admin/accounts/raw", requireAdmin, (_req, res) => {
  res.json({ accounts: Accounts.listAccounts() });
});

app.post("/api/admin/accounts", requireAdmin, (req, res) => {
  try {
    const { token, recipeId, label } = 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() || "",
    });
    res.json({ account: acc });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

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

app.delete("/api/admin/accounts/:id", requireAdmin, (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/clean", requireAdmin, (_req, res) => {
  const count = Accounts.deleteExpiredAccounts();
  res.json({ ok: true, cleaned: count });
});

// Bulk import: paste many tokens at once (one per line, optionally TAB-separated with recipeId & label)
app.post("/api/admin/accounts/bulk", requireAdmin, (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) => {
  res.json({ vouchers: Vouchers.listVouchers() });
});

app.post("/api/admin/vouchers", requireAdmin, (req, res) => {
  try {
    const { code, tier, whatsapp, duration, durationDays, dailyLimit } = req.body || {};
    const vch = Vouchers.addVoucher({ code, tier, whatsapp, duration, durationDays, dailyLimit });
    res.json({ voucher: vch });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

app.delete("/api/admin/vouchers/:code", requireAdmin, (req, res) => {
  const ok = Vouchers.deleteVoucher(req.params.code);
  if (!ok) return res.status(404).json({ error: "Not found" });
  res.json({ ok: true });
});

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

app.get("/api/config", (_req, res) => {
  const accs = Accounts.listAccountsSafe();
  const available = accs.filter((a) => !a.expired).length;
  res.json({
    defaultRecipeId: DEFAULT_RECIPE_ID,
    totalAccounts: accs.length,
    availableAccounts: available,
  });
});

// ─── 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 = Accounts.pickAccount();
    if (!acc) return res.status(503).json({ error: "Tidak ada akun Weavy yang tersedia. Hubungi admin." });

    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 });
    }
    res.json({ ...data.json, _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 {
    const {
      imageUrl, imageMeta,
      videoUrl, videoMeta,
      prompt = "",
      modelType = "Standard",
      version = "V2.6",
      keepOriginalSound = true,
      characterOrientation = "image",
      numberOfRuns = 1,
    } = req.body || {};

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

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

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

    while (attempts < 3) {
      const acc = Accounts.pickAccount(triedIds);
      if (!acc) break;

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

      const recipeId = acc.recipeId || DEFAULT_RECIPE_ID;

      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);
        Vouchers.incrementUsage(req.voucher.code);
        return res.json({
          ...data.json,
          _accountId: acc.id,
          _accountLabel: acc.label,
          _recipeId: recipeId,
        });
      }

      // 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...`);
        Accounts.updateAccount(acc.id, { expired: true });
        lastError = extractWeavyError(data);
        continue;
      }

      if (r.status === 403) {
        console.warn(`Account ${acc.label} got 403 (recipe access denied), trying next...`);
        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), accountUsed: acc.label });
    }

    // All accounts tried
    if (attempts === 0) {
      return res.status(503).json({ error: "Tidak ada akun Weavy yang tersedia. Jalankan bot di /admin." });
    }
    return res.status(403).json({
      error: lastError || "Semua akun tidak bisa akses recipe. Jalankan bot untuk akun baru.",
      triedAccounts: attempts,
    });

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


// Poll: official Weavy status endpoint
//   GET /api/v1/batches/recipes/{recipeId}/batches/{batchId}/status
app.get("/api/poll/:accountId/:batchId", async (req, res) => {
  const { accountId, batchId } = req.params;
  const recipeIdQ = req.query.recipeId;

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

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

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

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

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

// ─── KIE.AI Direct Routes ───────────────────────────────────────────────────

const KIE_BASE = "https://api.kie.ai";

function kieHeaders() {
  const key = process.env.KIE_API_KEY || "";
  return {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${key}`,
  };
}

// Check if KIE API key is configured
app.get("/api/kie/config", (_req, res) => {
  const key = process.env.KIE_API_KEY || "";
  res.json({
    configured: key.length > 0,
    masked: key ? key.substring(0, 6) + "****" + key.slice(-4) : null,
  });
});

// Save KIE API key at runtime
app.post("/api/kie/key", (req, res) => {
  const { apiKey } = req.body || {};
  if (!apiKey) return res.status(400).json({ error: "apiKey required" });
  process.env.KIE_API_KEY = apiKey.trim();
  res.json({ ok: true, masked: apiKey.trim().substring(0, 6) + "****" + apiKey.trim().slice(-4) });
});

// Upload file to KIE.AI (base64 method)
app.post("/api/kie/upload", requireVoucher, upload.single("file"), async (req, res) => {
  try {
    if (!req.file) return res.status(400).json({ error: "No file provided" });
    const key = process.env.KIE_API_KEY || "";
    if (!key) return res.status(400).json({ error: "KIE_API_KEY not configured" });

    const mimeType = req.file.mimetype;
    const base64Data = `data:${mimeType};base64,${req.file.buffer.toString("base64")}`;
    const fileName = req.file.originalname;

    const r = await fetch(`${KIE_BASE}/api/file-base64-upload`, {
      method: "POST",
      headers: kieHeaders(),
      body: JSON.stringify({
        base64Data,
        uploadPath: "kling-motion/uploads",
        fileName,
      }),
    });

    const data = await r.json();
    if (!r.ok || (data.code !== 200 && data.success !== true)) {
      return res.status(r.status || 400).json({ error: data.msg || "Upload failed" });
    }
    res.json({ ok: true, url: data.data.downloadUrl, fileName: data.data.fileName });
  } catch (err) {
    console.error("KIE upload error:", err);
    res.status(500).json({ error: err.message });
  }
});

app.post("/api/kie/generate", requireVoucher, async (req, res) => {
  try {
    const key = process.env.KIE_API_KEY || "";
    if (!key) return res.status(400).json({ error: "KIE_API_KEY not configured. Set it in Settings." });

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

    const {
      imageUrl,
      videoUrl,
      prompt = "",
      characterOrientation = "video",
      mode = "720p",
    } = req.body || {};

    if (!imageUrl) return res.status(400).json({ error: "imageUrl required" });
    if (!videoUrl) return res.status(400).json({ error: "videoUrl required" });
    if (!["image", "video"].includes(characterOrientation))
      return res.status(400).json({ error: "characterOrientation must be 'image' or 'video'" });
    if (!["720p", "1080p"].includes(mode))
      return res.status(400).json({ error: "mode must be '720p' or '1080p'" });

    const payload = {
      model: "kling-2.6/motion-control",
      input: {
        input_urls: [imageUrl],
        video_urls: [videoUrl],
        character_orientation: characterOrientation,
        mode,
      },
    };
    if (prompt.trim()) payload.input.prompt = prompt.trim();

    const r = await fetch(`${KIE_BASE}/api/v1/jobs/createTask`, {
      method: "POST",
      headers: kieHeaders(),
      body: JSON.stringify(payload),
    });

    const data = await r.json();
    if (!r.ok || data.code !== 200) {
      return res.status(r.status || 400).json({ error: data.msg || "Failed to create task", code: data.code });
    }

    Vouchers.incrementUsage(req.voucher.code);
    console.log(`KIE task created: ${data.data.taskId}`);
    res.json({ ok: true, taskId: data.data.taskId });
  } catch (err) {
    console.error("KIE generate error:", err);
    res.status(500).json({ error: err.message });
  }
});

// Poll KIE task status
app.get("/api/kie/task/:taskId", async (req, res) => {
  try {
    const key = process.env.KIE_API_KEY || "";
    if (!key) return res.status(400).json({ error: "KIE_API_KEY not configured" });

    const { taskId } = req.params;
    const r = await fetch(`${KIE_BASE}/api/v1/jobs/recordInfo?taskId=${encodeURIComponent(taskId)}`, {
      headers: kieHeaders(),
    });

    const data = await r.json();
    if (!r.ok || data.code !== 200) {
      return res.status(r.status || 400).json({ error: data.msg || "Failed to get task", code: data.code });
    }

    const task = data.data;
    const state = task.state || "waiting";

    let resultUrl = null;
    if (state === "success" && task.resultJson) {
      try {
        const parsed = JSON.parse(task.resultJson);
        resultUrl = (parsed.resultUrls && parsed.resultUrls[0]) || null;
      } catch {}
    }

    res.json({
      ok: true,
      taskId,
      state,
      resultUrl,
      failMsg: task.failMsg || null,
      progress: task.progress || null,
      costTime: task.costTime || null,
    });
  } catch (err) {
    console.error("KIE poll error:", err);
    res.status(500).json({ error: err.message });
  }
});

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

// Sync manually with remote server
app.post("/api/admin/accounts/sync", requireAdmin, async (_req, res) => {
  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", requireAdmin, (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();
  // 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", requireAdmin, async (req, res) => {
  const { force = true } = req.body || {};
  // Don't await — run in background, stream logs via SSE
  triggerAutoToken(force).catch(() => {});
  res.json({ ok: true, msg: "Bot dimulai. Pantau log via SSE stream." });
});

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

// Get/update scheduler config
app.get("/api/admin/auto-token/config", requireAdmin, (_req, res) => {
  res.json({
    enabled: process.env.AUTO_TOKEN_ENABLED !== "false",
    intervalMinutes: parseInt(process.env.AUTO_TOKEN_INTERVAL_MINUTES || "45"),
    headless: process.env.PLAYWRIGHT_HEADLESS !== "false",
  });
});

app.post("/api/admin/auto-token/config", requireAdmin, (req, res) => {
  const { enabled, intervalMinutes, headless } = req.body || {};
  let msgParts = [];

  if (headless !== undefined) {
    const headlessStr = headless ? "true" : "false";
    process.env.PLAYWRIGHT_HEADLESS = headlessStr;
    try {
      const envPath = path.join(__dirname, ".env");
      let content = fs.readFileSync(envPath, "utf8");
      const regex = /^PLAYWRIGHT_HEADLESS=.*$/m;
      if (regex.test(content)) {
        content = content.replace(regex, `PLAYWRIGHT_HEADLESS=${headlessStr}`);
      } else {
        content += `\nPLAYWRIGHT_HEADLESS=${headlessStr}`;
      }
      fs.writeFileSync(envPath, content, "utf8");
      msgParts.push(`Browser mode: ${headless ? "Sembunyikan" : "Tampilkan"}`);
    } catch (e) {
      console.error("Gagal update .env untuk headless:", e);
    }
  }

  if (enabled === true) {
    const mins = intervalMinutes || parseInt(process.env.AUTO_TOKEN_INTERVAL_MINUTES || "45");
    startScheduler(mins);
    msgParts.push(`Scheduler diaktifkan (${mins} menit)`);
  } else if (enabled === false) {
    stopScheduler();
    msgParts.push("Scheduler dimatikan");
  }

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

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

// ─── 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 AUTO_ENABLED = process.env.AUTO_TOKEN_ENABLED === "true";
const AUTO_INTERVAL = parseInt(process.env.AUTO_TOKEN_INTERVAL_MINUTES || "45");

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 (AUTO_ENABLED) {
    startScheduler(AUTO_INTERVAL);
    console.log(`🤖 Auto Token Bot: aktif (interval ${AUTO_INTERVAL} menit)`);
  } else {
    console.log(`🤖 Auto Token Bot: nonaktif (set AUTO_TOKEN_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);
  }
});
