const moduleTypes = new Map();
const { obsBrowserSourceCss } = require("./obs-browser-defaults");

function number(value, fallback, min, max) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) return fallback;
  return Math.min(max, Math.max(min, parsed));
}

function text(value, fallback = "", max = 2000) {
  return String(value === undefined || value === null ? fallback : value).slice(0, max);
}

function color(value, fallback = "#ffffff") {
  const normalized = text(value, fallback, 32).trim();
  return /^(#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})|rgba?\([0-9.,%\s]+\)|transparent)$/i.test(normalized)
    ? normalized
    : fallback;
}

function url(value, { allowEmpty = true } = {}) {
  const normalized = text(value, "", 2048).trim();
  if (!normalized && allowEmpty) return "";
  try {
    const parsed = new URL(normalized);
    return ["http:", "https:"].includes(parsed.protocol) ? parsed.toString() : "";
  } catch {
    return "";
  }
}

function commonConfig(input = {}) {
  const anchors = [
    "top-left", "top-center", "top-right",
    "center-left", "center", "center-right",
    "bottom-left", "bottom-center", "bottom-right"
  ];
  return {
    x: number(input.x, 0, 0, 100),
    y: number(input.y, 0, 0, 100),
    width: number(input.width, 100, 0.1, 100),
    height: number(input.height, 100, 0.1, 100),
    opacity: number(input.opacity, 1, 0, 1),
    anchor: anchors.includes(input.anchor) ? input.anchor : "top-left"
  };
}

function mediaConfig(input = {}, { includeFit = false } = {}) {
  const playBehavior = ["once", "loop", "manual"].includes(input.playBehavior)
    ? input.playBehavior
    : "once";
  return {
    ...commonConfig(input),
    url: url(input.url),
    playBehavior,
    muted: input.muted === true || input.muted === "on",
    volume: number(input.volume, 1, 0, 1),
    playbackRate: number(input.playbackRate, 1, 0.25, 4),
    startAt: number(input.startAt, 0, 0, 86400),
    showControls: playBehavior === "manual" || input.showControls === true || input.showControls === "on",
    ...(includeFit ? { fit: ["contain", "cover", "fill"].includes(input.fit) ? input.fit : "contain" } : {})
  };
}

function registerOverlayModuleType(type) {
  if (!type?.id || typeof type.normalize !== "function") {
    throw new Error("Overlay module types require an id and normalize function.");
  }
  const renderType = type.renderType || type.id;
  if (!["text", "image", "video", "audio", "web", "chat"].includes(renderType)) {
    throw new Error("Overlay module renderType must be text, image, video, audio, web, or chat.");
  }
  moduleTypes.set(type.id, Object.freeze({ ...type, renderType }));
  return () => moduleTypes.delete(type.id);
}

function getOverlayModuleType(id) {
  return moduleTypes.get(String(id || "")) || null;
}

function listOverlayModuleTypes() {
  return Array.from(moduleTypes.values()).map(({ id, label, description, renderType }) => ({ id, label, description, renderType }));
}

function normalizeModuleConfig(typeId, input) {
  const type = getOverlayModuleType(typeId);
  if (!type) throw new Error("Unsupported overlay module type.");
  return type.normalize(input || {});
}

registerOverlayModuleType({
  id: "text",
  label: "Text",
  description: "Positioned text with configurable colors and sizing.",
  normalize(input) {
    return {
      ...commonConfig(input),
      text: text(input.text, "New text", 4000),
      fontSize: number(input.fontSize, 48, 8, 400),
      fontWeight: number(input.fontWeight, 700, 100, 900),
      color: color(input.color, "#ffffff"),
      background: color(input.background, "transparent"),
      horizontalAlign: ["left", "center", "right"].includes(input.horizontalAlign)
        ? input.horizontalAlign
        : (["left", "center", "right"].includes(input.align) ? input.align : "left"),
      verticalAlign: ["top", "center", "bottom"].includes(input.verticalAlign) ? input.verticalAlign : "center",
      overflow: ["wrap", "shrink", "clip", "single-line", "scroll"].includes(input.overflow)
        ? input.overflow
        : "wrap",
      padding: number(input.padding, 0, 0, 200)
    };
  }
});

registerOverlayModuleType({
  id: "image",
  label: "Image",
  description: "A responsive image from an HTTPS or HTTP URL.",
  normalize(input) {
    return {
      ...commonConfig(input),
      url: url(input.url),
      fit: ["contain", "cover", "fill"].includes(input.fit) ? input.fit : "contain"
    };
  }
});

registerOverlayModuleType({
  id: "chat",
  label: "Lumi chat",
  description: "Show live messages from one or more connected chat platforms and channels.",
  normalize(input) {
    const platforms = (Array.isArray(input.platforms) ? input.platforms : String(input.platforms || "twitch,youtube,discord").split(","))
      .map((entry) => text(entry, "", 24).trim().toLowerCase())
      .filter((entry, index, values) => ["twitch", "youtube", "discord"].includes(entry) && values.indexOf(entry) === index);
    const channels = (Array.isArray(input.channels) ? input.channels : String(input.channels || "").split(/[\n,]+/))
      .map((entry) => text(entry, "", 256).trim())
      .filter((entry, index, values) => entry && values.indexOf(entry) === index)
      .slice(0, 50);
    const blockedUsers = (Array.isArray(input.blockedUsers) ? input.blockedUsers : String(input.blockedUsers || "").split(/[\n,]+/))
      .map((entry) => text(entry, "", 320).trim())
      .filter((entry, index, values) => /^(?:twitch|youtube|discord|lumi):[^:]+$/i.test(entry) && values.indexOf(entry) === index)
      .slice(0, 100);
    if (input.platforms !== undefined && !platforms.length) throw new Error("Choose at least one chat service.");
    return {
      ...commonConfig(input),
      platforms: input.platforms === undefined ? ["twitch", "youtube", "discord"] : platforms,
      channels,
      blockedUsers,
      maxMessages: Math.round(number(input.maxMessages, 8, 1, 50)),
      messageTimeoutSeconds: number(input.messageTimeoutSeconds, 30, 0, 3600),
      newestAt: ["top", "bottom"].includes(input.newestAt) ? input.newestAt : "bottom",
      horizontalAlign: ["left", "center", "right"].includes(input.horizontalAlign) ? input.horizontalAlign : "left",
      fontFamily: text(input.fontFamily, "system-ui, sans-serif", 300).trim() || "system-ui, sans-serif",
      fontSize: number(input.fontSize, 32, 8, 160),
      fontWeight: number(input.fontWeight, 600, 100, 900),
      lineHeight: number(input.lineHeight, 1.25, 0.8, 3),
      color: color(input.color, "#ffffff"),
      usernameColor: color(input.usernameColor, "#67e8f9"),
      background: color(input.background, "transparent"),
      messageBackground: color(input.messageBackground, "#0f172bcc"),
      padding: number(input.padding, 16, 0, 100),
      gap: number(input.gap, 10, 0, 100),
      borderRadius: number(input.borderRadius, 12, 0, 100),
      showAvatars: input.showAvatars !== false && input.showAvatars !== "false" && input.showAvatars !== "off",
      showBadges: input.showBadges !== false && input.showBadges !== "false" && input.showBadges !== "off",
      showPlatform: input.showPlatform !== false && input.showPlatform !== "false" && input.showPlatform !== "off",
      showTimestamp: input.showTimestamp === true || input.showTimestamp === "on" || input.showTimestamp === "true",
      inAnimation: ["none", "fade", "scale", "slide-left", "slide-right", "slide-up", "slide-down"].includes(input.inAnimation) ? input.inAnimation : "slide-up",
      outAnimation: ["none", "fade", "scale", "slide-left", "slide-right", "slide-up", "slide-down"].includes(input.outAnimation) ? input.outAnimation : "fade",
      animationDurationMs: Math.round(number(input.animationDurationMs, 350, 0, 3000)),
      customCss: text(input.customCss, "", 12000)
    };
  }
});

registerOverlayModuleType({
  id: "video",
  label: "Video / media",
  description: "Play a video file, including its audio track, with scene-aware playback controls.",
  renderType: "video",
  normalize(input) {
    return mediaConfig(input, { includeFit: true });
  }
});

registerOverlayModuleType({
  id: "audio",
  label: "Audio",
  description: "Play an audio file through the OBS Browser Source.",
  renderType: "audio",
  normalize(input) {
    return mediaConfig(input);
  }
});

registerOverlayModuleType({
  id: "web",
  label: "Website or alert overlay",
  description: "Add a third-party alert box, chat overlay, or other web page.",
  normalize(input) {
    return {
      ...commonConfig(input),
      url: url(input.url),
      allowPointerEvents: input.allowPointerEvents === true || input.allowPointerEvents === "on",
      autoRefresh: input.autoRefresh === true || input.autoRefresh === "on",
      healthCheck: input.healthCheck !== false && input.healthCheck !== "false" && input.healthCheck !== "off",
      refreshIntervalSeconds: number(input.refreshIntervalSeconds, 300, 30, 3600),
      retryLimit: Math.round(number(input.retryLimit, 3, 0, 5)),
      cropTop: number(input.cropTop, 0, 0, 95),
      cropRight: number(input.cropRight, 0, 0, 95),
      cropBottom: number(input.cropBottom, 0, 0, 95),
      cropLeft: number(input.cropLeft, 0, 0, 95),
      zoom: number(input.zoom, 1, 0.1, 5),
      injectPageCss: input.injectPageCss !== false && input.injectPageCss !== "false" && input.injectPageCss !== "off",
      forceTransparentBackground: input.forceTransparentBackground !== false && input.forceTransparentBackground !== "false" && input.forceTransparentBackground !== "off",
      customCss: obsBrowserSourceCss(text(input.customCss, "", 12000))
    };
  }
});

module.exports = {
  getOverlayModuleType,
  listOverlayModuleTypes,
  normalizeModuleConfig,
  registerOverlayModuleType
};
