const { db } = require("./db");
const { getSetting } = require("./settings");
const { hasAccess } = require("./rbac");

const ROLE_LEVELS = Object.freeze({
  public: 0,
  user: 1,
  mod: 2,
  moderator: 2,
  admin: 3,
  internal: 4
});

const SENSITIVITY_LEVELS = Object.freeze({
  public_safe: 0,
  user: 1,
  moderator: 2,
  admin: 3,
  internal: 4,
  secret_never_render: 99
});

const VALUE_TYPES = new Set(["string", "number", "boolean", "url", "json", "date"]);
const placeholders = new Map();
const fieldPolicies = new Map();

function normalizeRole(value, fallback = "user") {
  const role = String(value || fallback).trim().toLowerCase();
  if (role === "moderator") return "mod";
  return Object.prototype.hasOwnProperty.call(ROLE_LEVELS, role) ? role : fallback;
}

function roleLevel(value) {
  return ROLE_LEVELS[normalizeRole(value, "public")] ?? 0;
}

function normalizeSensitivity(value, fallback = "public_safe") {
  const sensitivity = String(value || fallback).trim().toLowerCase();
  return Object.prototype.hasOwnProperty.call(SENSITIVITY_LEVELS, sensitivity)
    ? sensitivity
    : fallback;
}

function sensitivityLevel(value) {
  return SENSITIVITY_LEVELS[normalizeSensitivity(value)] ?? 0;
}

function normalizeId(value) {
  return String(value || "")
    .trim()
    .replace(/^\{\{\s*|\s*\}\}$/g, "")
    .replace(/[^A-Za-z0-9_.-]/g, "_")
    .replace(/_{2,}/g, "_")
    .replace(/^\.|\.$/g, "");
}

function tokenFor(id) {
  return `{{${normalizeId(id)}}}`;
}

function namespaceFor(id) {
  const parts = normalizeId(id).split(".").filter(Boolean);
  return parts.length > 1 ? parts.slice(0, -1).join(".") : parts[0] || "";
}

function normalizeStringList(value) {
  return (Array.isArray(value) ? value : [value])
    .flatMap((item) => String(item || "").split(/[,\n]/))
    .map((item) => item.trim())
    .filter(Boolean);
}

function normalizeDefinition(definition = {}) {
  const id = normalizeId(definition.id || definition.token);
  if (!id) {
    throw new Error("Placeholder id is required.");
  }
  const sensitivity = normalizeSensitivity(definition.sensitivity);
  if (sensitivity === "secret_never_render") {
    throw new Error("secret_never_render placeholders must not be registered.");
  }
  const namespace = normalizeId(definition.namespace || namespaceFor(id));
  const aliases = normalizeStringList(definition.aliases || [])
    .map(normalizeId)
    .filter((alias) => alias && alias !== id);
  return {
    id,
    token: tokenFor(id),
    namespace,
    aliases,
    label: String(definition.label || id).trim(),
    description: String(definition.description || "").trim(),
    value_type: VALUE_TYPES.has(definition.value_type) ? definition.value_type : "string",
    sensitivity,
    min_editor_role: normalizeRole(definition.min_editor_role || "user"),
    min_viewer_role: normalizeRole(definition.min_viewer_role || "user"),
    allowed_field_types: normalizeStringList(definition.allowed_field_types || []),
    cache_ttl_seconds: Math.max(0, Number(definition.cache_ttl_seconds) || 0),
    group: String(definition.group || namespace || "General").trim(),
    example: definition.example === undefined ? null : String(definition.example),
    plugin_id: definition.plugin_id ? String(definition.plugin_id).trim() : null,
    resolver: typeof definition.resolver === "function" ? definition.resolver : () => "",
    available: typeof definition.available === "function" ? definition.available : null
  };
}

function normalizePolicy(policy = {}) {
  const field_id = normalizeId(policy.field_id);
  if (!field_id) {
    throw new Error("Placeholder field policy id is required.");
  }
  return {
    field_id,
    label: String(policy.label || field_id).trim(),
    field_type: String(policy.field_type || "text").trim(),
    output_audience: normalizeRole(policy.output_audience || "user"),
    min_editor_role: normalizeRole(policy.min_editor_role || "user"),
    allowed_namespaces: normalizeStringList(policy.allowed_namespaces || []),
    allowed_placeholder_ids: normalizeStringList(policy.allowed_placeholder_ids || []).map(normalizeId),
    max_sensitivity: normalizeSensitivity(policy.max_sensitivity || "public_safe"),
    description: String(policy.description || "").trim()
  };
}

function registerPlaceholder(definition) {
  const normalized = normalizeDefinition(definition);
  placeholders.set(normalized.id, normalized);
  return normalized;
}

function registerPlaceholders(definitions = []) {
  return definitions.map(registerPlaceholder);
}

function unregisterPlaceholder(id) {
  return placeholders.delete(normalizeId(id));
}

function unregisterNamespace(namespace) {
  const normalized = normalizeId(namespace);
  for (const id of Array.from(placeholders.keys())) {
    const definition = placeholders.get(id);
    if (id === normalized || id.startsWith(`${normalized}.`) || definition?.namespace === normalized || definition?.namespace?.startsWith(`${normalized}.`)) {
      placeholders.delete(id);
    }
  }
}

function registerFieldPolicy(policy) {
  const normalized = normalizePolicy(policy);
  fieldPolicies.set(normalized.field_id, normalized);
  return normalized;
}

function getFieldPolicy(fieldId) {
  return fieldPolicies.get(normalizeId(fieldId)) || null;
}

function isPluginEnabled(pluginId) {
  if (!pluginId) return true;
  try {
    const row = db.prepare("SELECT enabled FROM plugins WHERE id = ?").get(pluginId);
    return !row || Boolean(row.enabled);
  } catch {
    return true;
  }
}

function userRole(user) {
  if (user?.isAdmin) return "admin";
  if (user?.isMod) return "mod";
  return user ? "user" : "public";
}

function roleAllows(user, role) {
  const normalized = normalizeRole(role, "user");
  if (normalized === "user") return Boolean(user);
  return hasAccess(user, normalized);
}

function namespaceAllowed(definition, policy) {
  if (policy.allowed_placeholder_ids.includes(definition.id)) return true;
  if (!policy.allowed_namespaces.length) return true;
  return policy.allowed_namespaces.some((namespace) => {
    const normalized = normalizeId(namespace);
    return definition.id === normalized ||
      definition.namespace === normalized ||
      definition.namespace.startsWith(`${normalized}.`) ||
      definition.id.startsWith(`${normalized}.`);
  });
}

function fieldTypeAllowed(definition, policy) {
  return !definition.allowed_field_types.length ||
    definition.allowed_field_types.includes(policy.field_type);
}

function availabilityAllows(definition, context) {
  if (!isPluginEnabled(definition.plugin_id)) return false;
  if (!definition.available) return true;
  try {
    return Boolean(definition.available(context));
  } catch {
    return false;
  }
}

function checkPlaceholderAccess(definition, policy, { user, outputAudience, runtimeContext } = {}) {
  if (!definition || !policy) {
    return { allowed: false, reason: "unknown_placeholder" };
  }
  const audience = normalizeRole(outputAudience || policy.output_audience, policy.output_audience);
  if (definition.sensitivity === "secret_never_render") {
    return { allowed: false, reason: "secret_never_render" };
  }
  const runtimeOnly = Boolean(runtimeContext?.runtime);
  if (!runtimeOnly && (!roleAllows(user, policy.min_editor_role) || !roleAllows(user, definition.min_editor_role))) {
    return { allowed: false, reason: "editor_role_forbidden" };
  }
  if (roleLevel(audience) < roleLevel(definition.min_viewer_role)) {
    return { allowed: false, reason: "viewer_role_forbidden" };
  }
  if (sensitivityLevel(definition.sensitivity) > sensitivityLevel(policy.max_sensitivity)) {
    return { allowed: false, reason: "sensitivity_forbidden" };
  }
  if (!namespaceAllowed(definition, policy)) {
    return { allowed: false, reason: "namespace_forbidden" };
  }
  if (!fieldTypeAllowed(definition, policy)) {
    return { allowed: false, reason: "field_type_forbidden" };
  }
  if (!availabilityAllows(definition, { user, policy, outputAudience: audience, runtimeContext })) {
    return { allowed: false, reason: "placeholder_unavailable" };
  }
  return { allowed: true, reason: "allowed" };
}

function findDefinition(token) {
  const id = normalizeId(token);
  if (placeholders.has(id)) return placeholders.get(id);
  for (const definition of placeholders.values()) {
    if (definition.aliases.includes(id)) return definition;
  }
  return null;
}

function parsePlaceholders(template) {
  const found = [];
  const matcher = /\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g;
  let match = null;
  while ((match = matcher.exec(String(template || "")))) {
    found.push({
      token: match[0],
      id: normalizeId(match[1]),
      index: match.index
    });
  }
  return found;
}

function catalog({ fieldId, user, outputAudience, runtimeContext } = {}) {
  const policy = getFieldPolicy(fieldId);
  if (!policy) return { policy: null, placeholders: [] };
  const audience = normalizeRole(outputAudience || policy.output_audience, policy.output_audience);
  const available = Array.from(placeholders.values())
    .filter((definition) => checkPlaceholderAccess(definition, policy, {
      user,
      outputAudience: audience,
      runtimeContext
    }).allowed)
    .map((definition) => ({
      id: definition.id,
      token: definition.token,
      namespace: definition.namespace,
      label: definition.label,
      description: definition.description,
      value_type: definition.value_type,
      sensitivity: definition.sensitivity,
      group: definition.group,
      example: definition.sensitivity === "public_safe" ? definition.example : null
    }))
    .sort((a, b) => a.token.localeCompare(b.token));
  return {
    policy: {
      field_id: policy.field_id,
      label: policy.label,
      field_type: policy.field_type,
      output_audience: audience
    },
    placeholders: available
  };
}

function validateTemplate({ fieldId, template, user, outputAudience, runtimeContext } = {}) {
  const policy = getFieldPolicy(fieldId);
  const errors = [];
  if (!policy) {
    return {
      ok: false,
      errors: [{ token: "", reason: "unknown_field_policy" }]
    };
  }
  for (const token of parsePlaceholders(template)) {
    const definition = findDefinition(token.id);
    const access = checkPlaceholderAccess(definition, policy, {
      user,
      outputAudience,
      runtimeContext
    });
    if (!access.allowed) {
      errors.push({
        token: token.token,
        id: token.id,
        reason: access.reason
      });
    }
  }
  return { ok: errors.length === 0, errors };
}

async function renderTemplate({ fieldId, template, user, outputAudience, runtimeContext, fallback = "[unavailable]" } = {}) {
  const policy = getFieldPolicy(fieldId);
  if (!policy) {
    return {
      ok: false,
      rendered: String(template || ""),
      errors: [{ token: "", reason: "unknown_field_policy" }]
    };
  }
  const errors = [];
  const rendered = await replaceAsync(String(template || ""), /\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g, async (match, rawId) => {
    const id = normalizeId(rawId);
    const definition = findDefinition(id);
    const access = checkPlaceholderAccess(definition, policy, {
      user,
      outputAudience,
      runtimeContext
    });
    if (!access.allowed) {
      errors.push({ token: match, id, reason: access.reason });
      return fallback;
    }
    try {
      const value = await withTimeout(Promise.resolve(definition.resolver({
        user,
        policy,
        outputAudience: normalizeRole(outputAudience || policy.output_audience, policy.output_audience),
        runtimeContext,
        token: match,
        id
      })), runtimeContext?.placeholder_timeout_ms);
      return stringifyResolvedValue(value);
    } catch (error) {
      errors.push({ token: match, id, reason: error?.code === "EPLACEHOLDERTIMEOUT" ? "resolver_timeout" : "resolver_failed" });
      return fallback;
    }
  });
  return { ok: errors.length === 0, rendered, errors };
}

function stringifyResolvedValue(value) {
  if (value === null || value === undefined) return "";
  if (typeof value === "string") return value;
  if (typeof value === "number" || typeof value === "boolean") return String(value);
  return JSON.stringify(value);
}

function replaceAsync(value, matcher, replacer) {
  const source = String(value || "");
  const flags = matcher.flags.includes("g") ? matcher.flags : `${matcher.flags}g`;
  const expression = new RegExp(matcher.source, flags);
  const matches = [...source.matchAll(expression)];
  return matches.reduce(async (pending, match) => {
    const state = await pending;
    const replacement = await replacer(...match, match.index, source, match.groups);
    return {
      output: `${state.output}${source.slice(state.cursor, match.index)}${replacement}`,
      cursor: match.index + match[0].length
    };
  }, Promise.resolve({ output: "", cursor: 0 })).then((state) => `${state.output}${source.slice(state.cursor)}`);
}

function withTimeout(promise, requestedMs) {
  const timeoutMs = Math.max(50, Math.min(Number(requestedMs) || 1500, 10000));
  let timer = null;
  const timeout = new Promise((_resolve, reject) => {
    timer = setTimeout(() => {
      const error = new Error("Placeholder resolver timed out.");
      error.code = "EPLACEHOLDERTIMEOUT";
      reject(error);
    }, timeoutMs);
  });
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}

function registerCorePlaceholders() {
  registerFieldPolicy({
    field_id: "core.custom_commands.static_response",
    label: "Custom command response",
    field_type: "command_response",
    output_audience: "user",
    min_editor_role: "mod",
    allowed_namespaces: ["core.main", "core.command", "custom", "user.public"],
    max_sensitivity: "public_safe"
  });
  registerPlaceholders([
    {
      id: "core.main.bot_name",
      namespace: "core.main",
      label: "Bot name",
      description: "Current Lumi site/bot display name.",
      value_type: "string",
      sensitivity: "public_safe",
      min_editor_role: "user",
      min_viewer_role: "user",
      allowed_field_types: ["chat_message", "command_response", "admin_template", "okf_markdown"],
      example: "Lumi",
      resolver: () => getSetting("site_title", "Lumi")
    },
    {
      id: "core.main.command_prefix",
      namespace: "core.main",
      label: "Command prefix",
      description: "Configured chat command prefix.",
      value_type: "string",
      sensitivity: "public_safe",
      min_editor_role: "user",
      min_viewer_role: "user",
      allowed_field_types: ["chat_message", "command_response", "admin_template", "okf_markdown"],
      example: "!",
      resolver: () => getSetting("command_prefix", "!")
    },
    {
      id: "core.command.rng",
      namespace: "core.command",
      label: "Random Reply number",
      description: "The number rolled for the current Random Reply command. Blank for other messages.",
      value_type: "number",
      sensitivity: "public_safe",
      min_editor_role: "mod",
      min_viewer_role: "user",
      allowed_field_types: ["command_response"],
      example: "42",
      resolver: ({ runtimeContext }) => runtimeContext?.command?.rng ?? ""
    },
    {
      id: "user.public.display_name",
      namespace: "user.public",
      label: "Viewer display name",
      description: "Display name of the user who triggered the message, when available.",
      value_type: "string",
      sensitivity: "public_safe",
      min_editor_role: "user",
      min_viewer_role: "user",
      allowed_field_types: ["chat_message", "command_response", "okf_markdown"],
      resolver: ({ runtimeContext }) =>
        runtimeContext?.user?.displayName ||
        runtimeContext?.user?.username ||
        runtimeContext?.ctx?.user?.displayName ||
        runtimeContext?.ctx?.user?.username ||
        ""
    }
  ]);
  registerCustomPlaceholders(getSetting("custom_placeholders", []));
}

function validateCustomPlaceholders(value) {
  const rows = Array.isArray(value) ? value : [];
  const errors = [];
  const definitions = [];
  const names = new Set();
  if (rows.length > 50) errors.push("You can save at most 50 custom placeholders.");
  rows.slice(0, 50).forEach((row, index) => {
    const name = String(row?.name || "").trim().toLowerCase().replace(/^custom\./, "");
    const rawValue = String(row?.value ?? "");
    const visibility = ["user", "mod", "admin"].includes(row?.visibility) ? row.visibility : "";
    const description = String(row?.description || "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim().slice(0, 240);
    if (!/^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*$/.test(name)) {
      errors.push(`Custom placeholder ${index + 1} needs a lowercase name such as stream.title.`);
      return;
    }
    if (!visibility) {
      errors.push(`Choose who may receive custom.${name}.`);
      return;
    }
    if (/(?:^|[._])(?:api_?key|client_?secret|credential|password|private_?key|secret|token)(?:$|[._])/.test(name)) {
      errors.push(`custom.${name} looks like a secret. Passwords, tokens, credentials, and API keys must not be placeholders.`);
      return;
    }
    if (names.has(name)) {
      errors.push(`Custom placeholder custom.${name} is listed more than once.`);
      return;
    }
    if (rawValue.length > 2000) {
      errors.push(`Custom placeholder custom.${name} is longer than 2,000 characters.`);
      return;
    }
    names.add(name);
    definitions.push({
      name,
      value: rawValue.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").slice(0, 2000),
      visibility,
      description
    });
  });
  return { ok: errors.length === 0, errors, definitions };
}

function registerCustomPlaceholders(value) {
  unregisterNamespace("custom");
  const validation = validateCustomPlaceholders(value);
  if (!validation.ok) return validation;
  const roles = {
    user: { viewer: "user", sensitivity: "public_safe" },
    mod: { viewer: "mod", sensitivity: "moderator" },
    admin: { viewer: "admin", sensitivity: "admin" }
  };
  registerPlaceholders(validation.definitions.map((definition) => ({
    id: `custom.${definition.name}`,
    namespace: "custom",
    label: definition.name.replace(/[._-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()),
    description: definition.description || `Admin-managed custom value: custom.${definition.name}`,
    value_type: "string",
    sensitivity: roles[definition.visibility].sensitivity,
    min_editor_role: definition.visibility === "user" ? "mod" : definition.visibility,
    min_viewer_role: roles[definition.visibility].viewer,
    allowed_field_types: ["command_response", "admin_template", "okf_markdown"],
    group: "Custom",
    example: definition.visibility === "user" ? definition.value.slice(0, 120) : null,
    resolver: () => definition.value
  })));
  return validation;
}

function registerPlatformPlaceholders({ discordClient, getTwitchClient, getYouTubeClient } = {}) {
  registerPlaceholders([
    ...discordPlatformPlaceholders(discordClient),
    ...twitchPlatformPlaceholders(getTwitchClient),
    ...youtubePlatformPlaceholders(getYouTubeClient)
  ]);
}

function platformPlaceholder(base) {
  return {
    namespace: base.namespace,
    sensitivity: base.sensitivity || "public_safe",
    min_editor_role: base.min_editor_role || "user",
    min_viewer_role: base.min_viewer_role || "user",
    allowed_field_types: base.allowed_field_types || ["okf_markdown", "admin_template"],
    group: base.group,
    ...base
  };
}

function discordPlatformPlaceholders(discordClient) {
  const namespace = "platform.discord.guild";
  const group = "Discord guild";
  const guildValue = async (reader) => {
    const guild = await resolveDiscordGuild(discordClient);
    if (!guild) return "";
    try {
      return reader(guild);
    } catch {
      return "";
    }
  };
  return [
    platformPlaceholder({
      id: `${namespace}.data_status`,
      namespace,
      group,
      label: "Discord data status",
      description: "Whether Discord values come from the connected runtime cache or are unavailable.",
      example: "live_cache",
      resolver: () => discordDataStatus(discordClient)
    }),
    platformPlaceholder({
      id: `${namespace}.name`,
      namespace,
      group,
      label: "Discord server name",
      description: "Name of the configured Discord server.",
      example: "Cozy Carnage",
      resolver: () => guildValue((guild) => guild.name || "")
    }),
    platformPlaceholder({
      id: `${namespace}.member_count`,
      namespace,
      group,
      label: "Discord member count",
      description: "Current member count reported by the Discord server cache/API.",
      value_type: "number",
      example: "128",
      resolver: () => guildValue((guild) => guild.memberCount ?? guild.approximateMemberCount ?? "")
    }),
    platformPlaceholder({
      id: `${namespace}.created_at`,
      namespace,
      group,
      label: "Discord server created at",
      description: "ISO datetime when the configured Discord server was created.",
      value_type: "date",
      example: "2024-01-01T12:00:00.000Z",
      resolver: () => guildValue((guild) => guild.createdAt?.toISOString?.() || (guild.createdTimestamp ? new Date(guild.createdTimestamp).toISOString() : ""))
    }),
    platformPlaceholder({
      id: `${namespace}.text_channel_count`,
      namespace,
      group,
      label: "Discord text channel count",
      description: "Number of text-like channels currently cached for the configured Discord server.",
      value_type: "number",
      example: "12",
      resolver: () => guildValue((guild) => countDiscordChannels(guild, "text"))
    }),
    platformPlaceholder({
      id: `${namespace}.voice_channel_count`,
      namespace,
      group,
      label: "Discord voice channel count",
      description: "Number of voice/stage channels currently cached for the configured Discord server.",
      value_type: "number",
      example: "4",
      resolver: () => guildValue((guild) => countDiscordChannels(guild, "voice"))
    }),
    platformPlaceholder({
      id: `${namespace}.channel_count`,
      namespace,
      group,
      label: "Discord channel count",
      description: "Total number of channels currently cached for the configured Discord server.",
      value_type: "number",
      example: "18",
      resolver: () => guildValue((guild) => collectionSize(guild.channels?.cache))
    }),
    platformPlaceholder({
      id: `${namespace}.role_count`,
      namespace,
      group,
      label: "Discord role count",
      description: "Number of roles currently cached for the configured Discord server.",
      value_type: "number",
      example: "8",
      resolver: () => guildValue((guild) => collectionSize(guild.roles?.cache))
    }),
    platformPlaceholder({
      id: `${namespace}.emoji_count`,
      namespace,
      group,
      label: "Discord emoji count",
      description: "Number of custom emojis currently cached for the configured Discord server.",
      value_type: "number",
      example: "24",
      resolver: () => guildValue((guild) => collectionSize(guild.emojis?.cache))
    }),
    platformPlaceholder({
      id: `${namespace}.boost_count`,
      namespace,
      group,
      label: "Discord boost count",
      description: "Premium subscription count reported by Discord when available.",
      value_type: "number",
      example: "3",
      resolver: () => guildValue((guild) => guild.premiumSubscriptionCount ?? "")
    })
  ];
}

function twitchPlatformPlaceholders(getTwitchClient) {
  const namespace = "platform.twitch.channel";
  const group = "Twitch channel";
  return [
    platformPlaceholder({
      id: `${namespace}.data_status`,
      namespace,
      group,
      label: "Twitch data status",
      description: "Whether Twitch values come from a connected chat runtime, configured local values, or are unavailable.",
      example: "connected",
      resolver: () => getTwitchClient?.() ? "connected" : twitchChannels().length ? "configured_not_connected" : "unavailable"
    }),
    platformPlaceholder({
      id: `${namespace}.primary_name`,
      namespace,
      group,
      label: "Twitch primary channel",
      description: "First configured Twitch channel name.",
      example: "cozycarnage",
      resolver: () => firstTwitchChannel()
    }),
    platformPlaceholder({
      id: `${namespace}.configured_count`,
      namespace,
      group,
      label: "Twitch configured channel count",
      description: "Number of configured Twitch channels.",
      value_type: "number",
      example: "1",
      resolver: () => twitchChannels().length
    }),
    platformPlaceholder({
      id: `${namespace}.bot_username`,
      namespace,
      group,
      label: "Twitch bot username",
      description: "Configured Twitch bot username.",
      example: "lumi_bot",
      resolver: () => getSetting("twitch_bot_username", "") || ""
    }),
    platformPlaceholder({
      id: `${namespace}.connected`,
      namespace,
      group,
      label: "Twitch bot connected",
      description: "Whether the Twitch chat client is currently connected.",
      value_type: "boolean",
      example: "true",
      resolver: () => Boolean(getTwitchClient?.())
    })
  ];
}

function youtubePlatformPlaceholders(getYouTubeClient) {
  const namespace = "platform.youtube.channel";
  const group = "YouTube channel";
  return [
    platformPlaceholder({
      id: `${namespace}.data_status`,
      namespace,
      group,
      label: "YouTube data status",
      description: "Whether YouTube values come from the connected runtime, configured local values, or are unavailable.",
      example: "connected",
      resolver: () => getYouTubeClient?.()
        ? "connected"
        : getSetting("youtube_bot_channel_id", "") ? "configured_not_connected" : "unavailable"
    }),
    platformPlaceholder({
      id: `${namespace}.id`,
      namespace,
      group,
      label: "YouTube channel ID",
      description: "Configured or hydrated YouTube bot channel ID.",
      example: "UC...",
      resolver: () => getYouTubeClient?.()?.channelId || getSetting("youtube_bot_channel_id", "") || ""
    }),
    platformPlaceholder({
      id: `${namespace}.name`,
      namespace,
      group,
      label: "YouTube channel name",
      description: "Hydrated YouTube bot channel name when the integration is connected.",
      example: "Cozy Carnage",
      resolver: () => getYouTubeClient?.()?.channelName || ""
    }),
    platformPlaceholder({
      id: `${namespace}.live_chat_active`,
      namespace,
      group,
      label: "YouTube live chat active",
      description: "Whether Lumi currently has an active YouTube live chat ID.",
      value_type: "boolean",
      example: "false",
      resolver: () => Boolean(getYouTubeClient?.()?.liveChatId)
    }),
    platformPlaceholder({
      id: `${namespace}.connected`,
      namespace,
      group,
      label: "YouTube bot connected",
      description: "Whether the YouTube chat integration runtime is currently active.",
      value_type: "boolean",
      example: "true",
      resolver: () => Boolean(getYouTubeClient?.())
    })
  ];
}

async function resolveDiscordGuild(discordClient) {
  const guildId = getSetting("discord_guild_id", "");
  if (!discordClient || !guildId) return null;
  return discordClient.guilds?.cache?.get?.(guildId) || null;
}

function discordDataStatus(discordClient) {
  const guildId = getSetting("discord_guild_id", "");
  if (!guildId) return "unavailable";
  return discordClient?.guilds?.cache?.get?.(guildId) ? "live_cache" : "configured_not_connected";
}

function countDiscordChannels(guild, kind) {
  const channels = Array.from(guild.channels?.cache?.values?.() || []);
  return channels.filter((channel) => {
    const type = channel?.type;
    if (kind === "voice") {
      return type === 2 || type === 13 || /voice|stage/i.test(String(type));
    }
    return [0, 5, 10, 11, 12, 15, 16].includes(type) || /text|news|announcement|forum|media|thread/i.test(String(type));
  }).length;
}

function collectionSize(collection) {
  if (!collection) return "";
  if (typeof collection.size === "number") return collection.size;
  if (Array.isArray(collection)) return collection.length;
  return "";
}

function twitchChannels() {
  return String(getSetting("twitch_channels", "") || "")
    .split(/[,\s]+/)
    .map((entry) => entry.trim().replace(/^#/, ""))
    .filter(Boolean);
}

function firstTwitchChannel() {
  return twitchChannels()[0] || "";
}

module.exports = {
  catalog,
  checkPlaceholderAccess,
  getFieldPolicy,
  parsePlaceholders,
  registerCorePlaceholders,
  registerCustomPlaceholders,
  registerFieldPolicy,
  registerPlaceholder,
  registerPlaceholders,
  registerPlatformPlaceholders,
  renderTemplate,
  unregisterNamespace,
  validateCustomPlaceholders,
  unregisterPlaceholder,
  validateTemplate,
  _internals: {
    normalizeId,
    normalizePolicy,
    normalizeDefinition,
    placeholders,
    fieldPolicies
  }
};
