const crypto = require("crypto");
const { subscribeWebEvents } = require("./web-events");
const { createLogger } = require("./logger");
const {
  OVERLAY_CAPABILITIES,
  canAccessOverlay
} = require("./overlay-permissions");
const { getOverlayModuleType, listOverlayModuleTypes, normalizeModuleConfig } = require("./overlay-modules");
const {
  addModule,
  addScene,
  buildPublicState,
  chatDockRenderUrl,
  createOverlay,
  deleteModule,
  deleteOverlay,
  deleteScene,
  duplicateModule,
  duplicateOverlay,
  duplicateScene,
  getObsSettings,
  getOverlay,
  listOverlays,
  regenerateOverlayToken,
  regenerateSceneToken,
  refreshModule,
  reorderModules,
  reorderOverlays,
  reorderScenes,
  resolvePublicOverlay,
  resolveChatDockTicket,
  resolveWebSourceTicket,
  saveObsSettings,
  setActiveScene,
  updateModule,
  updateModuleTransform,
  updateOverlay,
  updateScene,
  webSourceRenderUrl
} = require("./overlays");
const { OBS_BROWSER_DEFAULT_CSS } = require("./obs-browser-defaults");
const { fetchPublicOverlay, loadInjectedOverlayDocument } = require("./overlay-web-documents");
const { recentOverlayChatMessages } = require("./overlay-chat");
const { listEventTypes } = require("./lumi-events");
const { twitchEventSubManager } = require("./twitch-eventsub");
const {
  ACTION_TYPES: OVERLAY_EVENT_ACTIONS,
  createOverlayEventHook,
  deleteOverlayEventHook,
  listOverlayEventHooks,
  testOverlayEventHook,
  updateOverlayEventHook
} = require("./overlay-event-hooks");
const {
  listOverlayConnectorProviders,
  overlayConnectorManager
} = require("./overlay-connectors");

const externalHealthCache = new Map();
const overlayWebLog = createLogger("core:overlay-web", { category: "integration" });
const overlayPreviewDrafts = new Map();

function createOverlayPreviewDraft({ overlayId, moduleId, config }) {
  const now = Date.now();
  for (const [ticket, draft] of overlayPreviewDrafts) {
    if (draft.expiresAt <= now || (draft.overlayId === overlayId && draft.moduleId === moduleId)) overlayPreviewDrafts.delete(ticket);
  }
  const ticket = crypto.randomBytes(24).toString("base64url");
  overlayPreviewDrafts.set(ticket, { overlayId, moduleId, config, expiresAt: now + 10 * 60 * 1000 });
  return ticket;
}

async function probeExternalOverlay(url) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 8000);
  try {
    const response = await fetchPublicOverlay(url, controller.signal);
    if (!response) {
      return {
        healthy: true,
        skipped: true,
        status: null,
        reason: "Private and local website addresses are refreshed but not probed by Lumi."
      };
    }
    if (!response.ok) {
      response.body?.cancel?.().catch?.(() => {});
      return { healthy: false, status: response.status, reason: `The website returned HTTP ${response.status}.` };
    }
    const contentType = String(response.headers.get("content-type") || "").toLowerCase();
    if (contentType && !/(html|xhtml|text|javascript|json|svg)/i.test(contentType)) {
      response.body?.cancel?.().catch?.(() => {});
      return { healthy: false, status: response.status, reason: "The website returned an unexpected content type." };
    }
    const reader = response.body?.getReader?.();
    let sample = "";
    if (reader) {
      const decoder = new TextDecoder();
      while (sample.length < 65536) {
        const { done, value } = await reader.read();
        if (done) break;
        sample += decoder.decode(value, { stream: true });
      }
      await reader.cancel().catch(() => {});
    }
    if (!sample.trim()) {
      return { healthy: false, status: response.status, reason: "The website returned an empty reply." };
    }
    if (contentType.includes("html") && !/<(?:!doctype|html|head|body|script|style|div|canvas|svg)\b/i.test(sample)) {
      return { healthy: false, status: response.status, reason: "The website reply did not look like a usable overlay page." };
    }
    return {
      healthy: true,
      status: response.status,
      reason: null,
      checked_at: Date.now(),
      etag: response.headers.get("etag") || null,
      last_modified: response.headers.get("last-modified") || null
    };
  } catch (error) {
    return {
      healthy: false,
      status: null,
      reason: error?.name === "AbortError" ? "The website took too long to reply." : "The website could not be reached."
    };
  } finally {
    clearTimeout(timeout);
  }
}

function noStore(res) {
  res.set("Cache-Control", "no-store, private");
  res.set("Pragma", "no-cache");
}

function browserSourceResponseHeaders(res) {
  noStore(res);
  res.set("Referrer-Policy", "no-referrer");
  res.set("Cross-Origin-Resource-Policy", "cross-origin");
  res.set("Content-Security-Policy", [
    "sandbox allow-scripts allow-forms allow-popups allow-presentation",
    "default-src * data: blob: 'unsafe-inline' 'unsafe-eval'",
    "connect-src * data: blob: ws: wss:",
    "font-src * data: blob:",
    "frame-src * data: blob:",
    "img-src * data: blob:",
    "media-src * data: blob:",
    "script-src * data: blob: 'unsafe-inline' 'unsafe-eval'",
    "style-src * data: blob: 'unsafe-inline'",
    "worker-src * data: blob:"
  ].join("; "));
}

function browserSourceErrorHtml(message) {
  const safe = String(message || "The website could not be loaded.")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;");
  return `<!doctype html><html><head><style>${OBS_BROWSER_DEFAULT_CSS}body{color:#fff;font:600 16px/1.4 system-ui,sans-serif;padding:18px;box-sizing:border-box}.error{display:inline-block;padding:10px 12px;border:1px solid rgba(248,113,113,.8);border-radius:8px;background:rgba(15,23,42,.9)}</style></head><body><div class="error">${safe}</div></body></html>`;
}

function publicOverlayContext(req) {
  return resolvePublicOverlay(req.params.overlayToken, req.params.sceneToken || null);
}

function registerPublicOverlayRoutes(app) {
  app.get("/overlay-web/:ticket", async (req, res) => {
    browserSourceResponseHeaders(res);
    const source = resolveWebSourceTicket(req.params.ticket);
    if (!source?.config?.url || source.config.injectPageCss === false) {
      return res.status(404).send(browserSourceErrorHtml("Website source unavailable."));
    }
    try {
      const document = await loadInjectedOverlayDocument(source.config.url, source.config.customCss, {
        userAgent: req.get("user-agent"),
        acceptLanguage: req.get("accept-language"),
        forceTransparentBackground: source.config.forceTransparentBackground !== false
      });
      return res.type("html").send(document.html);
    } catch (error) {
      overlayWebLog.warn("Website CSS injection failed.", {
        module_id: source.moduleId,
        error_code: error?.code || null,
        error: error?.message || "Unknown website loading error."
      }, { event: "overlay.web_css_injection_failed" });
      return res.status(error?.code === "PRIVATE_TARGET" ? 422 : 502).type("html").send(browserSourceErrorHtml(error.message));
    }
  });

  const registerVariant = (basePath, fixed) => {
    app.post(`${basePath}/obs-bridge`, (req, res) => {
      noStore(res);
      const context = publicOverlayContext(req);
      if (!context || !context.overlay.enabled || (fixed && !context.fixedScene?.enabled)) {
        return res.status(404).json({ accepted: false, error: "Overlay unavailable." });
      }
      try {
        const result = overlayConnectorManager.reportBrowserBridge(context.overlay.id, req.body || {});
        return res.json({
          accepted: Boolean(result.accepted),
          enabled: Boolean(result.enabled),
          state: result.state || "disabled"
        });
      } catch (error) {
        return res.status(400).json({ accepted: false, error: error.message });
      }
    });

    app.get(`${basePath}/module-health`, async (req, res) => {
      noStore(res);
      const context = publicOverlayContext(req);
      if (!context) return res.status(404).json({ healthy: false, reason: "Overlay unavailable." });
      const state = buildPublicState(context.overlay.id, fixed ? context.fixedScene.id : null);
      const module = state.scene?.modules?.find((item) => item.id === req.query.module_id && item.renderType === "web");
      if (!module?.config?.url) return res.status(404).json({ healthy: false, reason: "Website source unavailable." });
      const cacheKey = `${context.overlay.id}:${module.id}`;
      const cached = externalHealthCache.get(cacheKey);
      if (cached && Date.now() - cached.at < 15000) return res.json(cached.result);
      const result = await probeExternalOverlay(module.config.url);
      externalHealthCache.set(cacheKey, { at: Date.now(), result });
      return res.status(result.healthy ? 200 : 502).json(result);
    });

    app.get(`${basePath}/state`, (req, res) => {
      noStore(res);
      const context = publicOverlayContext(req);
      if (!context) return res.status(404).json({ exists: false, enabled: false, scene: null });
      return res.json(buildPublicState(context.overlay.id, fixed ? context.fixedScene.id : null));
    });

    app.get(`${basePath}/events`, (req, res) => {
      const context = publicOverlayContext(req);
      if (!context) return res.status(404).end();
      return subscribeWebEvents(req, res, {
        scopes: [`overlay:${context.overlay.id}`],
        validate: () => Boolean(publicOverlayContext(req)),
        initialEvents: [
          { event: "overlay:changed", payload: { revision: context.overlay.updated_at } },
          ...recentOverlayChatMessages(context.overlay.id)
        ]
      });
    });

    app.get(basePath, (req, res) => {
      noStore(res);
      const context = publicOverlayContext(req);
      if (!context || !context.overlay.enabled || (fixed && !context.fixedScene.enabled)) {
        return res.status(404).send("Overlay unavailable.");
      }
      const statePath = `${req.path.replace(/\/$/, "")}/state`;
      const eventsPath = `${req.path.replace(/\/$/, "")}/events`;
      const healthPath = `${req.path.replace(/\/$/, "")}/module-health`;
      const bridgePath = `${req.path.replace(/\/$/, "")}/obs-bridge`;
      res.set("Content-Security-Policy", "default-src 'none'; img-src https: http: data:; media-src https: http: data: blob:; frame-src https: http:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self';");
      return res.render("overlay-render", {
        statePath,
        eventsPath,
        healthPath,
        bridgePath,
        obsBrowserDefaultCss: OBS_BROWSER_DEFAULT_CSS,
        initialStateJson: JSON.stringify(buildPublicState(context.overlay.id, fixed ? context.fixedScene.id : null)).replace(/</g, "\\u003c")
      });
    });
  };

  registerVariant("/overlay/:overlayToken/scene/:sceneToken", true);
  registerVariant("/overlay/:overlayToken", false);

  const chatDockContext = (req) => resolveChatDockTicket(req.params.ticket);
  const chatDockState = (context) => context ? {
    exists: true,
    enabled: true,
    revision: Date.now(),
    module: context.module
  } : { exists: false, enabled: false, revision: Date.now(), module: null };

  app.get("/overlay-chat/:ticket/state", (req, res) => {
    noStore(res);
    const context = chatDockContext(req);
    if (!context) return res.status(404).json(chatDockState(null));
    return res.json(chatDockState(context));
  });

  app.get("/overlay-chat/:ticket/events", (req, res) => {
    const context = chatDockContext(req);
    if (!context) return res.status(404).end();
    return subscribeWebEvents(req, res, {
      scopes: [`overlay:${context.overlayId}`],
      validate: () => Boolean(chatDockContext(req)),
      initialEvents: [
        { event: "overlay:changed", payload: { revision: Date.now() } },
        ...recentOverlayChatMessages(context.overlayId)
      ]
    });
  });

  app.get("/overlay-chat/:ticket", (req, res) => {
    noStore(res);
    const context = chatDockContext(req);
    if (!context) return res.status(404).send("Chat dock unavailable.");
    const path = req.path.replace(/\/$/, "");
    res.set("Content-Security-Policy", "default-src 'none'; img-src https: http: data:; media-src https: http: data: blob:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self';");
    return res.render("overlay-chat-dock", {
      statePath: `${path}/state`,
      eventsPath: `${path}/events`,
      initialStateJson: JSON.stringify(chatDockState(context)).replace(/</g, "\\u003c")
    });
  });
}

function requireOverlayAccess(capability = OVERLAY_CAPABILITIES.MANAGE) {
  return (req, res, next) => {
    if (!canAccessOverlay(req.session?.user, req.params.id, capability)) {
      if (req.path.startsWith("/api/")) return res.status(403).json({ error: "Access denied." });
      return res.status(403).render("error", { title: "Access denied", message: "You do not have access to that overlay." });
    }
    next();
  };
}

function requireSceneInOverlay(req, res, next) {
  const overlay = getOverlay(req.params.id);
  if (!overlay?.scenes.some((scene) => scene.id === req.params.sceneId)) {
    if (req.path.startsWith("/api/")) return res.status(404).json({ error: "Scene not found." });
    return res.status(404).render("error", { title: "Scene not found", message: "That scene is not part of this overlay." });
  }
  next();
}

function requireModuleInOverlay(req, res, next) {
  const overlay = getOverlay(req.params.id);
  if (!overlay?.scenes.some((scene) => scene.modules.some((module) => module.id === req.params.moduleId))) {
    if (req.path.startsWith("/api/")) return res.status(404).json({ error: "Source not found." });
    return res.status(404).render("error", { title: "Source not found", message: "That source is not part of this overlay." });
  }
  next();
}

function adminOnly(req, res, next) {
  if (!req.session?.user?.isAdmin) {
    if (req.path.startsWith("/api/")) return res.status(403).json({ error: "Access denied." });
    return res.status(403).render("error", { title: "Access denied", message: "Administrator access is required." });
  }
  next();
}

function flash(req, type, message) {
  req.session.flash = { type, message };
}

function redirectWithResult(req, res, pathValue, work, success) {
  try {
    const value = work();
    if (req.get("X-Lumi-Enhanced") === "1") {
      return res.json({ ok: true, message: success, fieldErrors: {}, data: { result: value }, patch: {}, redirect: null, reloadRequired: false });
    }
    flash(req, "success", success);
    return res.redirect(pathValue);
  } catch (error) {
    if (req.get("X-Lumi-Enhanced") === "1") {
      return res.status(422).json({ ok: false, message: error.message, fieldErrors: {}, data: {}, patch: {}, redirect: null, reloadRequired: false });
    }
    flash(req, "error", error.message);
    return res.redirect(pathValue);
  }
}

function booleanField(body, name) {
  return body[name] === true || body[name] === "true" || body[name] === "on" || body[name] === "1";
}

function orderedIds(body) {
  if (Array.isArray(body.ids)) return body.ids;
  if (typeof body.ids === "string") {
    try {
      const parsed = JSON.parse(body.ids);
      if (Array.isArray(parsed)) return parsed;
    } catch {}
    return body.ids.split(",").map((id) => id.trim()).filter(Boolean);
  }
  return [];
}

function moduleInput(body) {
  return {
    name: body.name,
    type: body.type,
    enabled: booleanField(body, "enabled"),
    config: {
      text: body.text,
      url: body.url,
      x: body.x,
      y: body.y,
      width: body.width,
      height: body.height,
      opacity: body.opacity,
      fontSize: body.font_size,
      fontWeight: body.font_weight,
      color: body.color,
      background: body.background,
      align: body.align,
      padding: body.padding,
      fit: body.fit,
      playBehavior: body.play_behavior,
      muted: booleanField(body, "muted"),
      volume: body.volume,
      playbackRate: body.playback_rate,
      startAt: body.start_at,
      showControls: booleanField(body, "show_controls"),
      allowPointerEvents: booleanField(body, "allow_pointer_events"),
      injectPageCss: booleanField(body, "inject_page_css"),
      forceTransparentBackground: booleanField(body, "force_transparent_background"),
      anchor: body.anchor,
      horizontalAlign: body.horizontal_align,
      verticalAlign: body.vertical_align,
      overflow: body.text_overflow,
      autoRefresh: booleanField(body, "auto_refresh"),
      healthCheck: booleanField(body, "health_check"),
      refreshIntervalSeconds: body.refresh_interval_seconds,
      retryLimit: body.retry_limit,
      cropTop: body.crop_top,
      cropRight: body.crop_right,
      cropBottom: body.crop_bottom,
      cropLeft: body.crop_left,
      zoom: body.zoom,
      platforms: ["twitch", "youtube", "discord"].filter((platform) => booleanField(body, `chat_platform_${platform}`)),
      channels: body.chat_channels,
      blockedUsers: body.chat_blocked_users,
      maxMessages: body.chat_max_messages,
      messageTimeoutSeconds: body.chat_timeout_seconds,
      newestAt: body.chat_newest_at,
      fontFamily: body.chat_font_family,
      lineHeight: body.chat_line_height,
      usernameColor: body.chat_username_color,
      messageBackground: body.chat_message_background,
      gap: body.chat_gap,
      borderRadius: body.chat_border_radius,
      showAvatars: booleanField(body, "chat_show_avatars"),
      showBadges: booleanField(body, "chat_show_badges"),
      showPlatform: booleanField(body, "chat_show_platform"),
      showTimestamp: booleanField(body, "chat_show_timestamp"),
      inAnimation: body.chat_in_animation,
      outAnimation: body.chat_out_animation,
      animationDurationMs: body.chat_animation_duration_ms,
      customCss: body.type === "chat" ? body.chat_custom_css : body.custom_css
    }
  };
}

function overlayEditorState(overlay) {
  return {
    overlayId: overlay.id,
    canvas: { width: overlay.canvas_width || 1920, height: overlay.canvas_height || 1080 },
    activeSceneId: overlay.active_scene_id,
    scenes: overlay.scenes.map((scene) => ({
      id: scene.id,
      name: scene.name,
      enabled: scene.enabled,
      modules: scene.modules.map((module) => ({
        id: module.id,
        name: module.name,
        type: module.type,
        renderType: getOverlayModuleType(module.type)?.renderType || module.type,
        enabled: module.enabled,
        config: {
          ...module.config,
          ...((getOverlayModuleType(module.type)?.renderType || module.type) === "web" && module.config?.injectPageCss !== false
            ? { renderUrl: webSourceRenderUrl(module.id) }
            : {})
        }
      }))
    }))
  };
}

function baseUrl(req) {
  return `${req.protocol}://${req.get("host")}`;
}

function registerOverlayAdminRoutes(app) {
  app.get("/admin/overlays/:id/modules/:moduleId/web-preview/:ticket", requireOverlayAccess(), requireModuleInOverlay, async (req, res) => {
    browserSourceResponseHeaders(res);
    const draft = overlayPreviewDrafts.get(req.params.ticket);
    if (!draft || draft.expiresAt <= Date.now() || draft.overlayId !== req.params.id || draft.moduleId !== req.params.moduleId) {
      overlayPreviewDrafts.delete(req.params.ticket);
      return res.status(404).send(browserSourceErrorHtml("This unsaved website preview expired."));
    }
    try {
      const document = await loadInjectedOverlayDocument(draft.config.url, draft.config.customCss, {
        userAgent: req.get("user-agent"),
        acceptLanguage: req.get("accept-language"),
        forceTransparentBackground: draft.config.forceTransparentBackground !== false
      });
      return res.type("html").send(document.html);
    } catch (error) {
      return res.status(error?.code === "PRIVATE_TARGET" ? 422 : 502).type("html").send(browserSourceErrorHtml(error.message));
    }
  });

  app.get("/admin/overlays", adminOnly, (req, res) => {
    noStore(res);
    res.render("admin-overlays", {
      title: "OBS overlays",
      overlays: listOverlays({ includeSecrets: true }),
      baseUrl: baseUrl(req)
    });
  });

  app.post("/admin/overlays", adminOnly, (req, res) => redirectWithResult(
    req,
    res,
    "/admin/overlays",
    () => createOverlay({ name: req.body.name, description: req.body.description, sceneName: req.body.scene_name || "Main" }),
    "Overlay created."
  ));

  app.post("/admin/overlays/reorder", adminOnly, (req, res) => redirectWithResult(req, res, "/admin/overlays", () => reorderOverlays(orderedIds(req.body)), "Overlay order saved."));

  app.get("/admin/overlays/:id", requireOverlayAccess(OVERLAY_CAPABILITIES.MANAGE), async (req, res) => {
    noStore(res);
    const canViewSecrets = canAccessOverlay(req.session.user, req.params.id, OVERLAY_CAPABILITIES.VIEW_SECRETS);
    const canManageObs = canAccessOverlay(req.session.user, req.params.id, OVERLAY_CAPABILITIES.MANAGE_OBS);
    const overlay = getOverlay(req.params.id, { includeSecrets: canViewSecrets });
    if (!overlay) return res.status(404).render("error", { title: "Overlay not found", message: "That overlay does not exist." });
    if (canViewSecrets) {
      for (const module of overlay.scenes.flatMap((scene) => scene.modules)) {
        if ((getOverlayModuleType(module.type)?.renderType || module.type) === "chat") {
          const path = chatDockRenderUrl(module.id);
          module.chat_dock_url = path ? `${baseUrl(req)}${path}` : "";
        }
      }
    }
    res.render("admin-overlay-detail", {
      title: overlay.name,
      overlay,
      canViewSecrets,
      canManageObs,
      baseUrl: baseUrl(req),
      obsBrowserDefaultCss: OBS_BROWSER_DEFAULT_CSS,
      moduleTypes: listOverlayModuleTypes(),
      eventTypes: listEventTypes(),
      eventActions: OVERLAY_EVENT_ACTIONS,
      eventHooks: listOverlayEventHooks(overlay.id),
      twitchEventStatus: twitchEventSubManager.getStatus(),
      editorStateJson: JSON.stringify(overlayEditorState(overlay)).replace(/</g, "\\u003c"),
      connectorProviders: canManageObs ? listOverlayConnectorProviders() : [],
      obsStatus: canManageObs
        ? await overlayConnectorManager.status(overlay.id, { includeScenes: true })
        : { state: "restricted", current_scene: null, error: null, scenes: [] }
    });
  });

  app.get("/admin/overlays/:id/preview-window", requireOverlayAccess(OVERLAY_CAPABILITIES.MANAGE), (req, res) => {
    noStore(res);
    const overlay = getOverlay(req.params.id, { includeSecrets: false });
    if (!overlay) return res.status(404).render("error", { title: "Overlay not found", message: "That overlay does not exist." });
    res.render("admin-overlay-preview", {
      title: `${overlay.name} preview`,
      overlay,
      editorStateJson: JSON.stringify(overlayEditorState(overlay)).replace(/</g, "\\u003c")
    });
  });

  app.post("/admin/overlays/:id", requireOverlayAccess(), (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => updateOverlay(req.params.id, {
    name: req.body.name,
    description: req.body.description,
    enabled: booleanField(req.body, "enabled"),
    canvasWidth: req.body.canvas_width,
    canvasHeight: req.body.canvas_height
  }), "Overlay saved."));

  app.post("/admin/overlays/:id/duplicate", requireOverlayAccess(), (req, res) => redirectWithResult(req, res, "/admin/overlays", () => duplicateOverlay(req.params.id, req.body.name), "Overlay duplicated with new private URLs."));
  app.post("/admin/overlays/:id/delete", requireOverlayAccess(), (req, res) => redirectWithResult(req, res, "/admin/overlays", () => deleteOverlay(req.params.id), "Overlay deleted."));
  app.post("/admin/overlays/:id/token/revoke", requireOverlayAccess(OVERLAY_CAPABILITIES.VIEW_SECRETS), (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => regenerateOverlayToken(req.params.id), "All Browser Source URLs for this overlay were regenerated. The old URLs are revoked."));
  app.post("/admin/overlays/:id/active-scene", requireOverlayAccess(OVERLAY_CAPABILITIES.CONTROL_SCENE), (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => setActiveScene(req.params.id, req.body.scene_id, { source: "admin" }), "Active overlay scene changed."));

  app.post("/admin/overlays/:id/scenes", requireOverlayAccess(), (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => addScene(req.params.id, { name: req.body.name, obsSceneName: req.body.obs_scene_name }), "Scene created."));
  app.post("/admin/overlays/:id/scenes/reorder", requireOverlayAccess(), (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => reorderScenes(req.params.id, orderedIds(req.body)), "Scene order saved."));
  app.post("/admin/overlays/:id/scenes/:sceneId", requireOverlayAccess(), requireSceneInOverlay, (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => updateScene(req.params.sceneId, { name: req.body.name, enabled: booleanField(req.body, "enabled"), obsSceneName: req.body.obs_scene_name }), "Scene saved."));
  app.post("/admin/overlays/:id/scenes/:sceneId/duplicate", requireOverlayAccess(), requireSceneInOverlay, (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => duplicateScene(req.params.sceneId, req.body.name), "Scene duplicated with a new private URL."));
  app.post("/admin/overlays/:id/scenes/:sceneId/delete", requireOverlayAccess(), requireSceneInOverlay, (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => deleteScene(req.params.sceneId), "Scene deleted."));
  app.post("/admin/overlays/:id/scenes/:sceneId/token/revoke", requireOverlayAccess(OVERLAY_CAPABILITIES.VIEW_SECRETS), requireSceneInOverlay, (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => regenerateSceneToken(req.params.sceneId), "Fixed scene URL regenerated. The old URL is revoked."));

  app.post("/admin/overlays/:id/scenes/:sceneId/modules", requireOverlayAccess(), requireSceneInOverlay, (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => addModule(req.params.sceneId, moduleInput(req.body)), "Source added."));
  app.post("/admin/overlays/:id/scenes/:sceneId/modules/reorder", requireOverlayAccess(), requireSceneInOverlay, (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => reorderModules(req.params.sceneId, orderedIds(req.body)), "Layer order saved."));
  app.post("/admin/overlays/:id/modules/:moduleId", requireOverlayAccess(), requireModuleInOverlay, (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => updateModule(req.params.moduleId, moduleInput(req.body)), "Source saved."));
  app.post("/admin/overlays/:id/modules/:moduleId/duplicate", requireOverlayAccess(), requireModuleInOverlay, (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => duplicateModule(req.params.moduleId, req.body.name), "Source copied."));
  app.post("/admin/overlays/:id/modules/:moduleId/delete", requireOverlayAccess(), requireModuleInOverlay, (req, res) => redirectWithResult(req, res, `/admin/overlays/${req.params.id}`, () => deleteModule(req.params.moduleId), "Source deleted."));

  const eventHookInput = (req) => ({
    overlayId: req.params.id,
    eventType: req.body.event_type,
    actionType: req.body.action_type,
    moduleId: req.body.module_id,
    durationSeconds: req.body.duration_seconds,
    tier: req.body.tier,
    enabled: booleanField(req.body, "enabled")
  });

  app.post("/admin/overlays/:id/event-hooks", requireOverlayAccess(), (req, res) => redirectWithResult(
    req,
    res,
    `/admin/overlays/${req.params.id}`,
    () => createOverlayEventHook(eventHookInput(req)),
    "Event alert created."
  ));
  app.post("/admin/overlays/:id/event-hooks/:hookId", requireOverlayAccess(), (req, res) => redirectWithResult(
    req,
    res,
    `/admin/overlays/${req.params.id}`,
    () => updateOverlayEventHook(req.params.hookId, eventHookInput(req)),
    "Event alert saved."
  ));
  app.post("/admin/overlays/:id/event-hooks/:hookId/test", requireOverlayAccess(), (req, res) => {
    const returnTo = `/admin/overlays/${req.params.id}`;
    try {
      const result = testOverlayEventHook(req.params.hookId, req.params.id);
      flash(req, result.delivered ? "success" : "warning", result.delivered
        ? `Test alert sent to ${result.delivered} connected Browser Source${result.delivered === 1 ? "" : "s"}.`
        : "No Browser Source is connected to this overlay. Open its private link in OBS or the pop-out preview, then test again.");
    } catch (error) {
      flash(req, "error", error.message);
    }
    res.redirect(returnTo);
  });
  app.post("/admin/overlays/:id/event-hooks/:hookId/delete", requireOverlayAccess(), (req, res) => redirectWithResult(
    req,
    res,
    `/admin/overlays/${req.params.id}`,
    () => deleteOverlayEventHook(req.params.hookId, req.params.id),
    "Event alert deleted."
  ));

  app.post("/api/admin/overlays/:id/modules/:moduleId/transform", requireOverlayAccess(), requireModuleInOverlay, (req, res) => {
    noStore(res);
    try {
      res.json({ ok: true, module: updateModuleTransform(req.params.moduleId, req.body || {}) });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.post("/api/admin/overlays/:id/modules/:moduleId/preview", requireOverlayAccess(), requireModuleInOverlay, (req, res) => {
    noStore(res);
    try {
      const input = moduleInput(req.body || {});
      const config = normalizeModuleConfig(input.type, input.config);
      const renderType = getOverlayModuleType(input.type)?.renderType || input.type;
      if (renderType === "web" && config.url && config.injectPageCss !== false) {
        const ticket = createOverlayPreviewDraft({ overlayId: req.params.id, moduleId: req.params.moduleId, config });
        config.renderUrl = `/admin/overlays/${encodeURIComponent(req.params.id)}/modules/${encodeURIComponent(req.params.moduleId)}/web-preview/${ticket}`;
      }
      return res.json({
        ok: true,
        module: {
          id: req.params.moduleId,
          name: String(input.name || "Source").trim().slice(0, 120) || "Source",
          type: input.type,
          renderType,
          enabled: input.enabled,
          config
        }
      });
    } catch (error) {
      return res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.post("/api/admin/overlays/:id/modules/:moduleId/refresh", requireOverlayAccess(), requireModuleInOverlay, (req, res) => {
    noStore(res);
    try {
      refreshModule(req.params.moduleId);
      res.json({ ok: true, message: "Source refresh requested." });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.get("/api/admin/overlays/:id/modules/:moduleId/health", requireOverlayAccess(), requireModuleInOverlay, async (req, res) => {
    noStore(res);
    const overlay = getOverlay(req.params.id);
    const module = overlay?.scenes.flatMap((scene) => scene.modules).find((item) => item.id === req.params.moduleId);
    if (!module || module.type !== "web" || !module.config?.url) {
      return res.status(404).json({ ok: false, healthy: false, reason: "Website source unavailable." });
    }
    const result = await probeExternalOverlay(module.config.url);
    return res.json({ ok: true, ...result });
  });

  app.post("/admin/overlays/:id/obs", requireOverlayAccess(OVERLAY_CAPABILITIES.MANAGE_OBS), async (req, res) => {
    try {
      saveObsSettings(req.params.id, {
        enabled: booleanField(req.body, "enabled"),
        provider: req.body.provider,
        endpoint: req.body.endpoint,
        password: req.body.password,
        clearPassword: booleanField(req.body, "clear_password"),
        syncDirection: req.body.sync_direction,
        reconnect: booleanField(req.body, "reconnect")
      });
      await overlayConnectorManager.restart(req.params.id);
      if (req.get("X-Lumi-Enhanced") === "1") {
        return res.json({
          ok: true,
          message: "OBS connector settings saved.",
          fieldErrors: {},
          data: { status: await overlayConnectorManager.status(req.params.id, { includeScenes: true }) },
          patch: {},
          redirect: null,
          reloadRequired: false
        });
      }
      flash(req, "success", "OBS connector settings saved.");
    } catch (error) {
      if (req.get("X-Lumi-Enhanced") === "1") {
        return res.status(422).json({ ok: false, message: error.message, fieldErrors: {}, data: {}, patch: {}, redirect: null, reloadRequired: false });
      }
      flash(req, "error", error.message);
    }
    res.redirect(`/admin/overlays/${req.params.id}`);
  });

  app.get("/api/admin/overlays/:id/obs/status", requireOverlayAccess(OVERLAY_CAPABILITIES.MANAGE_OBS), async (req, res) => {
    noStore(res);
    res.json({ ok: true, ...(await overlayConnectorManager.status(req.params.id, { includeScenes: true })) });
  });

  app.post("/api/admin/overlays/:id/obs/test", requireOverlayAccess(OVERLAY_CAPABILITIES.MANAGE_OBS), async (req, res) => {
    noStore(res);
    try {
      const existing = getObsSettings(req.params.id, { includeSecret: true });
      const result = await overlayConnectorManager.test({
        overlayId: req.params.id,
        provider: req.body.provider || existing.provider,
        endpoint: req.body.endpoint || existing.endpoint,
        password: req.body.password || existing.password
      });
      res.json(result);
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.post("/api/admin/overlays/:id/obs/scene", requireOverlayAccess(OVERLAY_CAPABILITIES.MANAGE_OBS), async (req, res) => {
    noStore(res);
    try {
      res.json({ ok: true, ...(await overlayConnectorManager.setObsScene(req.params.id, req.body.scene_name)) });
    } catch (error) {
      res.status(400).json({ ok: false, error: error.message });
    }
  });

  app.post("/api/admin/overlays/:id/obs/import", requireOverlayAccess(OVERLAY_CAPABILITIES.MANAGE_OBS), async (req, res) => {
    noStore(res);
    let syncSuppressed = false;
    try {
      const status = await overlayConnectorManager.status(req.params.id, { includeScenes: true });
      if (status.state !== "connected" || !status.scenes?.length) {
        throw new Error("Connect OBS with scene-reading permission before importing its setup.");
      }
      overlayConnectorManager.suppressSync(req.params.id, true);
      syncSuppressed = true;
      let overlay = getOverlay(req.params.id);
      let created = 0;
      let mapped = 0;
      for (const obsSceneName of status.scenes) {
        if (overlay.scenes.some((scene) => scene.obs_scene_name === obsSceneName)) continue;
        const sameName = overlay.scenes.find((scene) => !scene.obs_scene_name && scene.name.toLowerCase() === obsSceneName.toLowerCase());
        if (sameName) {
          updateScene(sameName.id, { name: sameName.name, enabled: sameName.enabled, obsSceneName });
          mapped += 1;
        } else {
          addScene(req.params.id, { name: obsSceneName, obsSceneName });
          created += 1;
        }
        overlay = getOverlay(req.params.id);
      }
      if (status.canvas_width && status.canvas_height) {
        updateOverlay(req.params.id, {
          name: overlay.name,
          description: overlay.description,
          enabled: overlay.enabled,
          canvasWidth: status.canvas_width,
          canvasHeight: status.canvas_height
        });
      }
      overlay = getOverlay(req.params.id);
      const current = overlay.scenes.find((scene) => scene.obs_scene_name === status.current_scene && scene.enabled);
      if (current && current.id !== overlay.active_scene_id) setActiveScene(req.params.id, current.id, { source: "obs" });
      return res.json({
        ok: true,
        created,
        mapped,
        canvas_updated: Boolean(status.canvas_width && status.canvas_height),
        editor: overlayEditorState(overlay),
        status: await overlayConnectorManager.status(req.params.id, { includeScenes: true }),
        message: `OBS setup applied: ${created} scene${created === 1 ? "" : "s"} added, ${mapped} existing scene${mapped === 1 ? "" : "s"} matched.`
      });
    } catch (error) {
      return res.status(400).json({ ok: false, error: error.message });
    } finally {
      if (syncSuppressed) overlayConnectorManager.suppressSync(req.params.id, false);
    }
  });
}

module.exports = {
  registerOverlayAdminRoutes,
  registerPublicOverlayRoutes
};
