const fs = require("fs");
const path = require("path");

const DEFAULT_ACTIONS = [
  { id: "hug", verb: "hugs", past: "hugged" },
  { id: "bonk", verb: "bonks", past: "bonked" },
  { id: "comfort", verb: "comforts", past: "comforted" },
  { id: "pat", verb: "pats", past: "patted" },
  { id: "cuddle", verb: "cuddles", past: "cuddled" },
  { id: "boop", verb: "boops", past: "booped" },
  { id: "highfive", verb: "high-fives", past: "high-fived", aliases: ["high-five", "hf"] },
  { id: "snuggle", verb: "snuggles", past: "snuggled" },
  { id: "cheer", verb: "cheers for", past: "cheered for" },
  { id: "headpat", verb: "headpats", past: "headpatted", aliases: ["head-pat"] },
  { id: "support", verb: "supports", past: "supported" },
  { id: "encourage", verb: "encourages", past: "encouraged" },
  { id: "stalk", verb: "stalks", past: "stalked", category: "yandere" },
  { id: "kidnap", verb: "kidnaps", past: "kidnapped", category: "yandere" },
  { id: "stab", verb: "stabs", past: "stabbed", category: "yandere" },
  { id: "claim", verb: "claims", past: "claimed", category: "yandere" }
];

const PLUGIN_ID = "expression-interaction";

let cachedConfig = null;
let cachedConfigAt = 0;

let cachedAppToken = null;
let cachedAppTokenExpiry = 0;
let refreshCommands = null;
let pluginMeta = { dir: __dirname, name: "Expression Interaction" };

module.exports = {
  id: PLUGIN_ID,
  init({ web, settings, db, commandRouter, plugin }) {
    ensureTables(db);
    ensureDefaultActions(db);
    pluginMeta = {
      dir: plugin?.dir || __dirname,
      name: plugin?.name || "Expression Interaction"
    };
    writeCommandsManifest(getExpressionConfig(db));
    refreshCommands = registerExpressionCommands({ commandRouter, settings, db });

    const router = web.createRouter();
    router.get("/", (req, res) => {
      const config = getExpressionConfig(db);
      const user = req.session.user || null;
      res.render(path.join(__dirname, "views", "expression.ejs"), {
        title: "Expression Interaction",
        actions: config.actions,
        platforms: config.platforms,
        conflicts: config.conflicts,
        stats: user ? getUserStats(db, user.id) : null,
        globalStats: getGlobalStats(db),
        isAdmin: Boolean(user?.isAdmin)
      });
    });
    router.post("/settings", (req, res) => {
      if (!req.session.user || !req.session.user.isAdmin) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      savePlatformSettings(db, req.body);
      if (refreshCommands) {
        refreshCommands();
      } else {
        writeCommandsManifest(getExpressionConfig(db));
      }
      req.session.flash = {
        type: "success",
        message: "Expression settings updated."
      };
      res.redirect("/plugins/expression-interaction");
    });
    router.post("/actions/create", (req, res) => {
      if (!req.session.user || !req.session.user.isAdmin) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      const result = createExpressionAction(db, req.body);
      if (!result.ok) {
        req.session.flash = { type: "error", message: result.message };
        return res.redirect("/plugins/expression-interaction");
      }
      if (refreshCommands) {
        refreshCommands();
      } else {
        writeCommandsManifest(getExpressionConfig(db));
      }
      req.session.flash = { type: "success", message: "Expression added." };
      res.redirect("/plugins/expression-interaction");
    });
    router.post("/actions/:id/update", (req, res) => {
      if (!req.session.user || !req.session.user.isAdmin) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      const result = updateExpressionAction(db, req.params.id, req.body);
      if (!result.ok) {
        req.session.flash = { type: "error", message: result.message };
        return res.redirect("/plugins/expression-interaction");
      }
      if (refreshCommands) {
        refreshCommands();
      } else {
        writeCommandsManifest(getExpressionConfig(db));
      }
      req.session.flash = { type: "success", message: "Expression updated." };
      res.redirect("/plugins/expression-interaction");
    });
    router.post("/actions/:id/toggle", (req, res) => {
      if (!req.session.user || !req.session.user.isAdmin) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      toggleExpressionAction(db, req.params.id);
      invalidateConfigCache();
      if (refreshCommands) {
        refreshCommands();
      } else {
        writeCommandsManifest(getExpressionConfig(db));
      }
      res.redirect("/plugins/expression-interaction");
    });
    router.post("/actions/:id/archive", (req, res) => {
      if (!req.session.user || !req.session.user.isAdmin) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      setExpressionActionArchived(db, req.params.id, true);
      invalidateConfigCache();
      if (refreshCommands) {
        refreshCommands();
      } else {
        writeCommandsManifest(getExpressionConfig(db));
      }
      res.redirect("/plugins/expression-interaction");
    });
    router.post("/actions/:id/restore", (req, res) => {
      if (!req.session.user || !req.session.user.isAdmin) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      setExpressionActionArchived(db, req.params.id, false);
      invalidateConfigCache();
      if (refreshCommands) {
        refreshCommands();
      } else {
        writeCommandsManifest(getExpressionConfig(db));
      }
      res.redirect("/plugins/expression-interaction");
    });
    web.mount("/plugins/expression-interaction", router, {
      label: "Expression Interaction",
      role: "public",
      section: "plugins"
    });
  }
};

function ensureTables(db) {
  db.exec(`
    CREATE TABLE IF NOT EXISTS expression_actions (
      id TEXT PRIMARY KEY,
      command TEXT NOT NULL,
      verb TEXT,
      past TEXT,
      aliases TEXT,
      enabled INTEGER NOT NULL DEFAULT 1,
      archived INTEGER NOT NULL DEFAULT 0,
      created_at INTEGER NOT NULL,
      updated_at INTEGER NOT NULL
    );

    CREATE TABLE IF NOT EXISTS expression_interactions (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      action TEXT NOT NULL,
      platform TEXT NOT NULL,
      actor_user_id TEXT NOT NULL,
      target_user_id TEXT NOT NULL,
      created_at INTEGER NOT NULL
    );

    CREATE TABLE IF NOT EXISTS expression_pair_stats (
      action TEXT NOT NULL,
      actor_user_id TEXT NOT NULL,
      target_user_id TEXT NOT NULL,
      count INTEGER NOT NULL DEFAULT 0,
      PRIMARY KEY (action, actor_user_id, target_user_id)
    );

    CREATE TABLE IF NOT EXISTS expression_user_stats (
      action TEXT NOT NULL,
      user_id TEXT NOT NULL,
      given_count INTEGER NOT NULL DEFAULT 0,
      received_count INTEGER NOT NULL DEFAULT 0,
      PRIMARY KEY (action, user_id)
    );
  `);
}

function parseBoolean(value, fallback) {
  if (value === undefined || value === null || value === "") {
    return fallback;
  }
  if (typeof value === "boolean") {
    return value;
  }
  const normalized = value.toString().toLowerCase();
  return ["1", "true", "yes", "on"].includes(normalized);
}

function getPluginSettings(db) {
  const rows = db
    .prepare("SELECT key, value FROM plugin_settings WHERE plugin_id = ?")
    .all(PLUGIN_ID);
  return rows.reduce((acc, row) => {
    acc[row.key] = row.value;
    return acc;
  }, {});
}

function setPluginSetting(db, key, value) {
  db.prepare(
    "INSERT INTO plugin_settings (plugin_id, key, value, updated_at) VALUES (?, ?, ?, ?) " +
      "ON CONFLICT(plugin_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at"
  ).run(PLUGIN_ID, key, value, Date.now());
}

function normalizeCommandName(name, fallback) {
  const raw = (name || fallback || "").trim().replace(/^!+/, "");
  if (!raw) {
    return (fallback || "").toLowerCase();
  }
  return raw.toLowerCase().replace(/\s+/g, "-");
}

function normalizeActionId(name) {
  const raw = (name || "").trim().replace(/^!+/, "").toLowerCase();
  if (!raw) {
    return "";
  }
  return raw
    .replace(/[^a-z0-9-_]+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-+|-+$/g, "");
}

function conjugateVerb(name) {
  const word = name.toLowerCase();
  if (word.endsWith("y") && !/[aeiou]y$/.test(word)) {
    return `${word.slice(0, -1)}ies`;
  }
  if (/(s|x|z|ch|sh)$/.test(word)) {
    return `${word}es`;
  }
  return `${word}s`;
}

function conjugatePast(name) {
  const word = name.toLowerCase();
  if (word.endsWith("e")) {
    return `${word}d`;
  }
  if (word.endsWith("y") && !/[aeiou]y$/.test(word)) {
    return `${word.slice(0, -1)}ied`;
  }
  return `${word}ed`;
}

function parseList(value) {
  return (value || "")
    .toString()
    .split(/[,\s]+/)
    .map((item) => item.trim())
    .filter(Boolean);
}

function parseAliasList(value) {
  if (value === undefined || value === null || value === "") {
    return [];
  }
  if (Array.isArray(value)) {
    return value.map((item) => item.toString());
  }
  try {
    const parsed = JSON.parse(value);
    if (Array.isArray(parsed)) {
      return parsed.map((item) => item.toString());
    }
  } catch {
    // ignore invalid JSON
  }
  return parseList(value);
}

function normalizeAliasList(list, command) {
  const seen = new Set();
  const normalized = [];
  for (const entry of list || []) {
    const alias = normalizeCommandName(entry, "");
    if (!alias || alias === command || seen.has(alias)) {
      continue;
    }
    seen.add(alias);
    normalized.push(alias);
  }
  return normalized;
}

function ensureDefaultActions(db) {
  const existing = db
    .prepare("SELECT COUNT(*) AS count FROM expression_actions")
    .get();
  const rows = existing?.count || 0;
  const settings = getPluginSettings(db);
  const now = Date.now();
  const insert = db.prepare(
    "INSERT INTO expression_actions (id, command, verb, past, aliases, enabled, archived, created_at, updated_at) " +
      "VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)"
  );

  const addAction = (action) => {
    const storedName = settings[`action_${action.id}_name`] || action.id;
    const command = normalizeCommandName(storedName, action.id);
    const enabled = parseBoolean(
      settings[`action_${action.id}_enabled`],
      true
    );
    const useDefaultAliases = command === normalizeCommandName(action.id, action.id);
    const aliases = useDefaultAliases
      ? normalizeAliasList(action.aliases || [], command)
      : [];
    insert.run(
      action.id,
      command,
      action.verb || "",
      action.past || "",
      JSON.stringify(aliases),
      enabled ? 1 : 0,
      now,
      now
    );
  };

  if (!rows) {
    DEFAULT_ACTIONS.forEach(addAction);
    return;
  }

  for (const action of DEFAULT_ACTIONS) {
    const existingAction = db
      .prepare("SELECT id FROM expression_actions WHERE id = ?")
      .get(action.id);
    if (!existingAction) {
      addAction(action);
    }
  }
}

function getExpressionActions(db) {
  const rows = db
    .prepare(
      "SELECT id, command, verb, past, aliases, enabled, archived, created_at, updated_at " +
        "FROM expression_actions ORDER BY created_at, id"
    )
    .all();
  return rows.map((row) => {
    const command = normalizeCommandName(row.command, row.id);
    const aliasList = normalizeAliasList(parseAliasList(row.aliases), command);
    const verbOverride = (row.verb || "").toString().trim();
    const pastOverride = (row.past || "").toString().trim();
    const verb = verbOverride || conjugateVerb(command);
    const past = pastOverride || conjugatePast(command);
    return {
      id: row.id,
      command,
      verb,
      past,
      verbOverride,
      pastOverride,
      aliases: aliasList,
      enabled: Boolean(row.enabled),
      archived: Boolean(row.archived),
      createdAt: row.created_at,
      updatedAt: row.updated_at
    };
  });
}

function getExpressionConfig(db) {
  const now = Date.now();
  if (cachedConfig && now - cachedConfigAt < 5000) {
    return cachedConfig;
  }

  const settings = getPluginSettings(db);
  const platforms = {
    discord: parseBoolean(settings.platform_discord, true),
    twitch: parseBoolean(settings.platform_twitch, true)
  };

  const conflicts = new Set();
  const actionByTrigger = new Map();
  const actions = getExpressionActions(db);
  actions
    .filter((action) => action.enabled && !action.archived)
    .forEach((action) => {
      const triggers = new Set([action.command, ...(action.aliases || [])]);
      for (const trigger of triggers) {
        if (actionByTrigger.has(trigger)) {
          conflicts.add(trigger);
          continue;
        }
        actionByTrigger.set(trigger, action);
      }
    });

  cachedConfig = {
    platforms,
    actions,
    actionByTrigger,
    conflicts: Array.from(conflicts)
  };
  cachedConfigAt = now;
  return cachedConfig;
}

function invalidateConfigCache() {
  cachedConfig = null;
  cachedConfigAt = 0;
}

function savePlatformSettings(db, body) {
  const platformDiscord = body.platform_discord === "on";
  const platformTwitch = body.platform_twitch === "on";
  setPluginSetting(db, "platform_discord", platformDiscord ? "1" : "0");
  setPluginSetting(db, "platform_twitch", platformTwitch ? "1" : "0");
  invalidateConfigCache();
}

function createExpressionAction(db, body) {
  const rawId = (body.action_id || "").trim();
  const rawCommand = (body.action_command || "").trim();
  const id = normalizeActionId(rawId || rawCommand);
  if (!id) {
    return { ok: false, message: "Action id is required." };
  }
  const existing = db
    .prepare("SELECT id FROM expression_actions WHERE id = ?")
    .get(id);
  if (existing) {
    return { ok: false, message: "That action id already exists." };
  }
  const command = normalizeCommandName(rawCommand || id, id);
  if (!command) {
    return { ok: false, message: "Command name is required." };
  }
  const verb = (body.action_verb || "").toString().trim();
  const past = (body.action_past || "").toString().trim();
  const aliases = normalizeAliasList(
    parseList(body.action_aliases || ""),
    command
  );
  const enabled = body.action_enabled === "on";
  const now = Date.now();
  db.prepare(
    "INSERT INTO expression_actions (id, command, verb, past, aliases, enabled, archived, created_at, updated_at) " +
      "VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)"
  ).run(
    id,
    command,
    verb,
    past,
    JSON.stringify(aliases),
    enabled ? 1 : 0,
    now,
    now
  );
  invalidateConfigCache();
  return { ok: true };
}

function updateExpressionAction(db, id, body) {
  const existing = db
    .prepare("SELECT id FROM expression_actions WHERE id = ?")
    .get(id);
  if (!existing) {
    return { ok: false, message: "Expression not found." };
  }
  const rawCommand = (body.action_command || "").trim();
  const command = normalizeCommandName(rawCommand || id, id);
  if (!command) {
    return { ok: false, message: "Command name is required." };
  }
  const verb = (body.action_verb || "").toString().trim();
  const past = (body.action_past || "").toString().trim();
  const aliases = normalizeAliasList(
    parseList(body.action_aliases || ""),
    command
  );
  const enabled = body.action_enabled === "on";
  const now = Date.now();
  db.prepare(
    "UPDATE expression_actions SET command = ?, verb = ?, past = ?, aliases = ?, enabled = ?, updated_at = ? WHERE id = ?"
  ).run(
    command,
    verb,
    past,
    JSON.stringify(aliases),
    enabled ? 1 : 0,
    now,
    id
  );
  invalidateConfigCache();
  return { ok: true };
}

function toggleExpressionAction(db, id) {
  const row = db
    .prepare("SELECT enabled FROM expression_actions WHERE id = ?")
    .get(id);
  if (!row) {
    return;
  }
  const next = row.enabled ? 0 : 1;
  db.prepare(
    "UPDATE expression_actions SET enabled = ?, updated_at = ? WHERE id = ?"
  ).run(next, Date.now(), id);
}

function setExpressionActionArchived(db, id, archived) {
  db.prepare(
    "UPDATE expression_actions SET archived = ?, updated_at = ? WHERE id = ?"
  ).run(archived ? 1 : 0, Date.now(), id);
}

function getUserStats(db, userId) {
  const rows = db
    .prepare(
      "SELECT action, given_count, received_count FROM expression_user_stats WHERE user_id = ?"
    )
    .all(userId);
  const totals = rows.reduce(
    (acc, row) => {
      acc.given += row.given_count;
      acc.received += row.received_count;
      return acc;
    },
    { given: 0, received: 0 }
  );
  const byAction = rows.reduce((acc, row) => {
    acc[row.action] = row;
    return acc;
  }, {});
  return { totals, byAction };
}

function getGlobalStats(db) {
  const total = db
    .prepare("SELECT COUNT(*) AS count FROM expression_interactions")
    .get();
  const byAction = db
    .prepare(
      "SELECT action, COUNT(*) AS count FROM expression_interactions GROUP BY action ORDER BY count DESC"
    )
    .all();
  return {
    total: total?.count || 0,
    byAction
  };
}

function registerExpressionCommands({ commandRouter, settings, db }) {
  if (!commandRouter) {
    return null;
  }

  const rebuild = () => {
    const config = getExpressionConfig(db);
    const platforms = [];
    if (config.platforms.discord) {
      platforms.push("discord");
    }
    if (config.platforms.twitch) {
      platforms.push("twitch");
    }

    if (!platforms.length) {
      writeCommandsManifest(config);
      commandRouter.registerCommands(PLUGIN_ID, []);
      return;
    }

    const commands = config.actions
      .filter((action) => action.enabled && !action.archived)
      .map((action) => {
        const triggers = new Set([action.command, ...(action.aliases || [])]);
        const filtered = Array.from(triggers).filter((trigger) => {
          const mapped = config.actionByTrigger.get(trigger);
          return mapped && mapped.id === action.id;
        });
        if (!filtered.length) {
          return null;
        }
        return {
          triggers: filtered,
          platforms,
          handler: async (ctx) => {
            return await handleExpressionCommand({
              ctx,
              actionId: action.id,
              settings,
              db
            });
          }
        };
      })
      .filter(Boolean);

    commandRouter.registerCommands(PLUGIN_ID, commands);
    writeCommandsManifest(config);
  };

  rebuild();
  return rebuild;
}

function writeCommandsManifest(config) {
  if (!pluginMeta?.dir) {
    return;
  }
  const toTitle = (value) =>
    (value || "").replace(/(^|\s|-)(\w)/g, (_m, sep, char) =>
      `${sep || ""}${char.toUpperCase()}`
    );
  const commands = (config.actions || [])
    .filter((action) => action.enabled && !action.archived)
    .map((action) => ({
      id: action.id,
      trigger: action.command,
      usage: `${action.command} <user>`,
      name: toTitle(action.command),
      description: `Send a ${action.command} to another user.`,
      level: "public",
      platforms: ["discord", "twitch"],
      aliases: action.aliases || []
    }));
  const manifest = {
    pluginId: PLUGIN_ID,
    pluginName: pluginMeta?.name || "Expression Interaction",
    platformKeys: {
      discord: "platform_discord",
      twitch: "platform_twitch"
    },
    commands
  };
  try {
    const target = path.join(pluginMeta.dir, "cmds.json");
    fs.writeFileSync(target, JSON.stringify(manifest, null, 2), "utf8");
  } catch (error) {
    console.error("Failed to write expression command manifest", error);
  }
}

async function handleExpressionCommand({ ctx, actionId, settings, db }) {
  const { ensureUserForIdentity } = require("../../src/services/users");
  const config = getExpressionConfig(db);
  if (!config.platforms[ctx.platform]) {
    return false;
  }
  const action = config.actions.find((item) => item.id === actionId);
  if (!action || !action.enabled || action.archived) {
    return false;
  }

  const prefix = settings.getSetting("command_prefix", "!");
  const targetToken = ctx.args[0];
  if (!targetToken) {
    const usageTarget = ctx.platform === "discord" ? "@username" : "username";
    await ctx.reply(`Usage: ${prefix}${action.command} ${usageTarget}`);
    return true;
  }

  if (ctx.platform === "discord") {
    const message = ctx.meta?.message;
    const targetInfo = await resolveDiscordTarget(
      message,
      targetToken,
      ensureUserForIdentity
    );
    if (!targetInfo) {
      await ctx.reply("I couldn't find that user. Try mentioning them.");
      return true;
    }
    const stats = recordInteraction(
      db,
      action.id,
      "discord",
      ctx.user.id,
      targetInfo.profile.id
    );
    const response = buildResponse({
      action,
      actorLabel: `<@${ctx.platformUser.id}>`,
      targetLabel: targetInfo.label,
      actorName: ctx.user.username,
      targetName: targetInfo.profile.internal_username,
      stats
    });
    await ctx.reply(response);
    return true;
  }

  if (ctx.platform === "twitch") {
    const targetLogin = targetToken.replace(/^@/, "").trim();
    if (!targetLogin) {
      await ctx.reply(`Usage: ${prefix}${action.command} username`);
      return true;
    }
    const targetResolved = await resolveTwitchTarget(
      targetLogin,
      settings,
      ensureUserForIdentity
    );
    const stats = recordInteraction(
      db,
      action.id,
      "twitch",
      ctx.user.id,
      targetResolved.profile.id
    );
    const response = buildResponse({
      action,
      actorLabel: `@${ctx.platformUser.username || ctx.platformUser.displayName}`,
      targetLabel: targetResolved.label,
      actorName: ctx.user.username,
      targetName: targetResolved.profile.internal_username,
      stats
    });
    await ctx.reply(response);
    return true;
  }

  return false;
}

async function resolveDiscordTarget(message, token, ensureUserForIdentity) {
  if (message?.mentions?.users?.first) {
    const mention = message.mentions.users.first();
    if (mention) {
      const display =
        mention.globalName || mention.username || mention.tag || mention.id;
      const profile = ensureUserForIdentity({
        provider: "discord",
        providerUserId: mention.id,
        displayName: display
      });
      return { profile, label: `<@${mention.id}>` };
    }
  }

  const idMatch = token.match(/^<@!?(\d+)>$/) || token.match(/^(\d{15,})$/);
  if (idMatch && message?.client?.users?.fetch) {
    const id = idMatch[1];
    const user = await message.client.users.fetch(id).catch(() => null);
    if (user) {
      const display = user.globalName || user.username || user.tag || user.id;
      const profile = ensureUserForIdentity({
        provider: "discord",
        providerUserId: user.id,
        displayName: display
      });
      return { profile, label: `<@${user.id}>` };
    }
  }

  const name = token.replace(/^@/, "").trim();
  if (!name) {
    return null;
  }
  const profile = ensureUserForIdentity({
    provider: "discord_name",
    providerUserId: name.toLowerCase(),
    displayName: name,
    fallbackName: name
  });
  return { profile, label: name };
}

function recordInteraction(db, action, platform, actorUserId, targetUserId) {
  const now = Date.now();
  db.prepare(
    "INSERT INTO expression_interactions (action, platform, actor_user_id, target_user_id, created_at) VALUES (?, ?, ?, ?, ?)"
  ).run(action, platform, actorUserId, targetUserId, now);

  db.prepare(
    "INSERT INTO expression_pair_stats (action, actor_user_id, target_user_id, count) VALUES (?, ?, ?, 1) " +
      "ON CONFLICT(action, actor_user_id, target_user_id) DO UPDATE SET count = count + 1"
  ).run(action, actorUserId, targetUserId);

  db.prepare(
    "INSERT INTO expression_user_stats (action, user_id, given_count, received_count) VALUES (?, ?, 1, 0) " +
      "ON CONFLICT(action, user_id) DO UPDATE SET given_count = given_count + 1"
  ).run(action, actorUserId);

  db.prepare(
    "INSERT INTO expression_user_stats (action, user_id, given_count, received_count) VALUES (?, ?, 0, 1) " +
      "ON CONFLICT(action, user_id) DO UPDATE SET received_count = received_count + 1"
  ).run(action, targetUserId);

  const pair = db
    .prepare(
      "SELECT count FROM expression_pair_stats WHERE action = ? AND actor_user_id = ? AND target_user_id = ?"
    )
    .get(action, actorUserId, targetUserId);
  const actorTotals = db
    .prepare(
      "SELECT given_count FROM expression_user_stats WHERE action = ? AND user_id = ?"
    )
    .get(action, actorUserId);
  const targetTotals = db
    .prepare(
      "SELECT received_count FROM expression_user_stats WHERE action = ? AND user_id = ?"
    )
    .get(action, targetUserId);
  const globalTotals = db
    .prepare("SELECT COUNT(*) AS count FROM expression_interactions WHERE action = ?")
    .get(action);

  return {
    pairCount: pair?.count || 1,
    actorTotal: actorTotals?.given_count || 1,
    targetTotal: targetTotals?.received_count || 1,
    globalTotal: globalTotals?.count || 1
  };
}

function buildResponse({ action, actorLabel, targetLabel, actorName, targetName, stats }) {
  const main = `${actorLabel} ${action.verb} ${targetLabel}.`;
  const options = [
    `${actorName} has ${action.past} ${targetName} ${stats.pairCount} times.`,
    `${actorName} has ${action.past} ${stats.actorTotal} times total.`,
    `${targetName} has been ${action.past} ${stats.targetTotal} times.`,
    `This action has been used ${stats.globalTotal} times.`
  ];
  const detail = options[Math.floor(Math.random() * options.length)];
  return `${main} ${detail}`;
}

async function resolveTwitchTarget(login, settings, ensureUserForIdentity) {
  const cleaned = login.toLowerCase();
  const resolved = await fetchTwitchUser(cleaned, settings);
  if (resolved) {
    const profile = ensureUserForIdentity({
      provider: "twitch",
      providerUserId: resolved.id,
      displayName: resolved.display_name
    });
    return { profile, label: `@${resolved.login || cleaned}` };
  }
  const profile = ensureUserForIdentity({
    provider: "twitch_login",
    providerUserId: cleaned,
    displayName: cleaned,
    fallbackName: cleaned
  });
  return { profile, label: `@${cleaned}` };
}

async function fetchTwitchUser(login, settings) {
  const clientId = settings.getSetting("twitch_client_id");
  const clientSecret = settings.getSetting("twitch_client_secret");
  if (!clientId || !clientSecret) {
    return null;
  }
  const token = await getTwitchAppToken(clientId, clientSecret);
  if (!token) {
    return null;
  }
  const response = await fetch(
    `https://api.twitch.tv/helix/users?login=${encodeURIComponent(login)}`,
    {
      headers: {
        "Client-Id": clientId,
        Authorization: `Bearer ${token}`
      }
    }
  );
  if (!response.ok) {
    return null;
  }
  const data = await response.json();
  return data.data?.[0] || null;
}

async function getTwitchAppToken(clientId, clientSecret) {
  const now = Date.now();
  if (cachedAppToken && now < cachedAppTokenExpiry) {
    return cachedAppToken;
  }
  const url =
    "https://id.twitch.tv/oauth2/token" +
    `?client_id=${encodeURIComponent(clientId)}` +
    `&client_secret=${encodeURIComponent(clientSecret)}` +
    "&grant_type=client_credentials";
  const response = await fetch(url, { method: "POST" });
  if (!response.ok) {
    return null;
  }
  const data = await response.json();
  if (!data.access_token || !data.expires_in) {
    return null;
  }
  cachedAppToken = data.access_token;
  cachedAppTokenExpiry = now + (data.expires_in - 60) * 1000;
  return cachedAppToken;
}
