const path = require("path");
const crypto = require("crypto");
const { ensureUserForIdentity } = require("../../src/services/users");


const PLUGIN_ID = "economy-games";
const LEGACY_STEM = ["echo", "nomy"].join("");
const LEGACY_PLUGIN_ID = `${LEGACY_STEM}-games`;
const DEFAULT_SETTINGS = {
  hotpotato_enabled: "1",
  hotpotato_platform_discord: "1",
  hotpotato_platform_twitch: "1",
  hotpotato_platform_youtube: "1",
  hotpotato_name: "Hot Potato",
  hotpotato_trigger: "hotpotato",
  hotpotato_aliases: "potato",
  hotpotato_min_cost: "10",
  hotpotato_max_cost: "250",
  hotpotato_toss_min: "10",
  hotpotato_toss_max: "25",
  hotpotato_loss_multiplier: "1",
  hotpotato_loss_additive: "0",
  hotpotato_presence_window: "300",
  coinflip_enabled: "1",
  coinflip_platform_discord: "1",
  coinflip_platform_twitch: "1",
  coinflip_platform_youtube: "1",
  coinflip_name: "Coinflip",
  coinflip_trigger: "coinflip",
  coinflip_aliases: "flip",
  coinflip_min_bet: "10",
  coinflip_max_bet: "500",
  coinflip_multiplier: "2",
  coinflip_cooldown: "10",
  mystery_enabled: "1",
  mystery_platform_discord: "1",
  mystery_platform_twitch: "1",
  mystery_platform_youtube: "1",
  mystery_name: "Mystery Box",
  mystery_trigger: "mysterybox",
  mystery_aliases: "box",
  mystery_min_bet: "10",
  mystery_max_bet: "500",
  mystery_multiplier: "2",
  mystery_cooldown: "10",
  responses_json: ""
};

const DEFAULT_RESPONSES = {
  hotpotato_start: [
    "{user} started {game} with {amount}. {target} has the potato! Toss within {seconds}s."
  ],
  hotpotato_toss: [
    "{user} tossed the potato to {target}. Toss within {seconds}s!"
  ],
  hotpotato_timeout: [
    "{loser} ran out of time and paid {loss} total. Winners: {winners}."
  ],
  hotpotato_no_targets: [
    "No active users to pass the potato to yet."
  ],
  hotpotato_already_active: [
    "{game} is already active. {holder} has the potato."
  ],
  hotpotato_not_holder: [
    "Only the current holder can toss the potato."
  ],
  hotpotato_not_active: [
    "{game} is not active yet. Start it with {trigger} <amount>."
  ],
  hotpotato_invalid_amount: [
    "Enter an amount between {min} and {max}."
  ],
  coinflip_win: [
    "{user} flipped heads and won {payout}!"
  ],
  coinflip_lose: [
    "{user} flipped tails and lost {amount}."
  ],
  coinflip_invalid: [
    "Enter a bet between {min} and {max}."
  ],
  coinflip_cooldown: [
    "Wait {seconds}s before flipping again."
  ],
  coinflip_insufficient: [
    "{reason}"
  ],
  mystery_result: [
    "{user} opened a box and got {payout} (from {amount})."
  ],
  mystery_invalid: [
    "Enter a bet between {min} and {max}."
  ],
  mystery_cooldown: [
    "Wait {seconds}s before opening another box."
  ],
  mystery_insufficient: [
    "{reason}"
  ]
};

const presence = {
  discord: new Map(),
  twitch: new Map(),
  youtube: new Map()
};

const hotPotatoGames = new Map();
const cooldowns = new Map();
let cachedConfig = null;
let cachedConfigAt = 0;

module.exports = {
  id: PLUGIN_ID,
  init({ web, settings, db, commandRouter, discordClient, twitchClient }) {
    migrateLegacyInstall(db);
    ensureDefaults(db);
    ensureStatsTable(db);
    const refreshCommands = registerCommands({ db, commandRouter, settings });

    attachDiscordPresence({ discordClient });
    attachTwitchPresence({ twitchClient });

    const router = web.createRouter();
    router.get("/", (req, res) => {
      const config = getConfig(db);
      const responses = buildResponses(db);
      const responsesByGame = {
        hotpotato: Object.values(responses).filter((entry) => entry.key.startsWith("hotpotato_")),
        coinflip: Object.values(responses).filter((entry) => entry.key.startsWith("coinflip_")),
        mystery: Object.values(responses).filter((entry) => entry.key.startsWith("mystery_"))
      };
      const framework = getFramework();
      const currencyLabel =
        framework?.getConfig?.().currency?.plural ||
        framework?.getConfig?.().currency?.name ||
        "Coins";
      const stats = {
        hotpotato: getGameStatsView(db, "hotpotato"),
        coinflip: getGameStatsView(db, "coinflip"),
        mystery: getGameStatsView(db, "mystery")
      };
      res.render(path.join(__dirname, "views", "games.ejs"), {
        title: "Economy Games",
        config,
        responses,
        responsesByGame,
        frameworkReady: Boolean(framework),
        currencyLabel,
        stats
      });
    });

    router.post("/settings/hotpotato", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      setPluginSetting(db, "hotpotato_enabled", req.body.hotpotato_enabled ? "1" : "0");
      setPluginSetting(
        db,
        "hotpotato_platform_discord",
        req.body.hotpotato_platform_discord ? "1" : "0"
      );
      setPluginSetting(
        db,
        "hotpotato_platform_twitch",
        req.body.hotpotato_platform_twitch ? "1" : "0"
      );
      setPluginSetting(
        db,
        "hotpotato_platform_youtube",
        req.body.hotpotato_platform_youtube ? "1" : "0"
      );
      setPluginSetting(db, "hotpotato_name", (req.body.hotpotato_name || "").trim());
      setPluginSetting(
        db,
        "hotpotato_trigger",
        (req.body.hotpotato_trigger || "").trim()
      );
      setPluginSetting(
        db,
        "hotpotato_aliases",
        (req.body.hotpotato_aliases || "").trim()
      );
      setPluginSetting(db, "hotpotato_min_cost", (req.body.hotpotato_min_cost || "0").trim());
      setPluginSetting(db, "hotpotato_max_cost", (req.body.hotpotato_max_cost || "0").trim());
      setPluginSetting(db, "hotpotato_toss_min", (req.body.hotpotato_toss_min || "1").trim());
      setPluginSetting(db, "hotpotato_toss_max", (req.body.hotpotato_toss_max || "1").trim());
      setPluginSetting(
        db,
        "hotpotato_loss_multiplier",
        (req.body.hotpotato_loss_multiplier || "1").trim()
      );
      setPluginSetting(
        db,
        "hotpotato_loss_additive",
        (req.body.hotpotato_loss_additive || "0").trim()
      );
      setPluginSetting(
        db,
        "hotpotato_presence_window",
        (req.body.hotpotato_presence_window || "300").trim()
      );
      invalidateConfigCache();
      refreshCommands?.();
      req.session.flash = { type: "success", message: "Hot potato updated." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/settings/coinflip", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      setPluginSetting(db, "coinflip_enabled", req.body.coinflip_enabled ? "1" : "0");
      setPluginSetting(
        db,
        "coinflip_platform_discord",
        req.body.coinflip_platform_discord ? "1" : "0"
      );
      setPluginSetting(
        db,
        "coinflip_platform_twitch",
        req.body.coinflip_platform_twitch ? "1" : "0"
      );
      setPluginSetting(
        db,
        "coinflip_platform_youtube",
        req.body.coinflip_platform_youtube ? "1" : "0"
      );
      setPluginSetting(db, "coinflip_name", (req.body.coinflip_name || "").trim());
      setPluginSetting(
        db,
        "coinflip_trigger",
        (req.body.coinflip_trigger || "").trim()
      );
      setPluginSetting(
        db,
        "coinflip_aliases",
        (req.body.coinflip_aliases || "").trim()
      );
      setPluginSetting(db, "coinflip_min_bet", (req.body.coinflip_min_bet || "0").trim());
      setPluginSetting(db, "coinflip_max_bet", (req.body.coinflip_max_bet || "0").trim());
      setPluginSetting(
        db,
        "coinflip_multiplier",
        (req.body.coinflip_multiplier || "2").trim()
      );
      setPluginSetting(
        db,
        "coinflip_cooldown",
        (req.body.coinflip_cooldown || "10").trim()
      );
      invalidateConfigCache();
      refreshCommands?.();
      req.session.flash = { type: "success", message: "Coinflip updated." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/settings/mystery", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      setPluginSetting(db, "mystery_enabled", req.body.mystery_enabled ? "1" : "0");
      setPluginSetting(
        db,
        "mystery_platform_discord",
        req.body.mystery_platform_discord ? "1" : "0"
      );
      setPluginSetting(
        db,
        "mystery_platform_twitch",
        req.body.mystery_platform_twitch ? "1" : "0"
      );
      setPluginSetting(
        db,
        "mystery_platform_youtube",
        req.body.mystery_platform_youtube ? "1" : "0"
      );
      setPluginSetting(db, "mystery_name", (req.body.mystery_name || "").trim());
      setPluginSetting(
        db,
        "mystery_trigger",
        (req.body.mystery_trigger || "").trim()
      );
      setPluginSetting(
        db,
        "mystery_aliases",
        (req.body.mystery_aliases || "").trim()
      );
      setPluginSetting(db, "mystery_min_bet", (req.body.mystery_min_bet || "0").trim());
      setPluginSetting(db, "mystery_max_bet", (req.body.mystery_max_bet || "0").trim());
      setPluginSetting(
        db,
        "mystery_multiplier",
        (req.body.mystery_multiplier || "2").trim()
      );
      setPluginSetting(
        db,
        "mystery_cooldown",
        (req.body.mystery_cooldown || "10").trim()
      );
      invalidateConfigCache();
      refreshCommands?.();
      req.session.flash = { type: "success", message: "Mystery box updated." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/settings/responses", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      const existing = loadCustomResponses(db);
      const payload = { ...existing };
      for (const key of Object.keys(DEFAULT_RESPONSES)) {
        const field = `response_${key}`;
        if (!Object.prototype.hasOwnProperty.call(req.body, field)) {
          continue;
        }
        const raw = (req.body[field] || "").toString();
        const lines = raw
          .split(/\r?\n/)
          .map((line) => line.trim())
          .filter(Boolean);
        if (lines.length) {
          payload[key] = lines;
        } else {
          delete payload[key];
        }
      }
      setPluginSetting(db, "responses_json", JSON.stringify(payload));
      invalidateConfigCache();
      req.session.flash = { type: "success", message: "Responses updated." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    web.mount(`/plugins/${PLUGIN_ID}`, router, {
      label: "Economy Games",
      role: "admin",
      section: "plugins"
    });
    mountLegacyRedirect(web, LEGACY_PLUGIN_ID, PLUGIN_ID);
  }
};

function deny(res) {
  return res.status(403).render("error", {
    title: "Access denied",
    message: "You do not have access to that page."
  });
}

function migrateLegacyInstall(db) {
  migratePluginSettings(db);
  mergeCommandUsage(db, `${LEGACY_PLUGIN_ID}:hotpotato`, `${PLUGIN_ID}:hotpotato`);
  mergeCommandUsage(db, `${LEGACY_PLUGIN_ID}:coinflip`, `${PLUGIN_ID}:coinflip`);
  mergeCommandUsage(db, `${LEGACY_PLUGIN_ID}:mystery`, `${PLUGIN_ID}:mystery`);
}

function migratePluginSettings(db) {
  const rows = db
    .prepare("SELECT key, value, updated_at FROM plugin_settings WHERE plugin_id = ?")
    .all(LEGACY_PLUGIN_ID);
  if (!rows.length) {
    return;
  }
  const insert = db.prepare(
    "INSERT OR IGNORE INTO plugin_settings (plugin_id, key, value, updated_at) VALUES (?, ?, ?, ?)"
  );
  for (const row of rows) {
    insert.run(PLUGIN_ID, row.key, row.value, row.updated_at || Date.now());
  }
  db.prepare("DELETE FROM plugin_settings WHERE plugin_id = ?").run(LEGACY_PLUGIN_ID);
}

function migrateTable(db, legacyName, currentName) {
  if (!tableExists(db, legacyName)) {
    return;
  }
  if (!tableExists(db, currentName)) {
    db.prepare(`ALTER TABLE ${quoteIdentifier(legacyName)} RENAME TO ${quoteIdentifier(currentName)}`).run();
    return;
  }
  db.prepare(
    `INSERT OR IGNORE INTO ${quoteIdentifier(currentName)} SELECT * FROM ${quoteIdentifier(legacyName)}`
  ).run();
  db.prepare(`DROP TABLE ${quoteIdentifier(legacyName)}`).run();
}

function mergeCommandUsage(db, legacyId, currentId) {
  if (!tableExists(db, "command_usage")) {
    return;
  }
  const legacy = db
    .prepare("SELECT count, updated_at FROM command_usage WHERE command_id = ?")
    .get(legacyId);
  if (!legacy) {
    return;
  }
  db.prepare(
    "INSERT INTO command_usage (command_id, count, updated_at) VALUES (?, ?, ?) " +
      "ON CONFLICT(command_id) DO UPDATE SET count = command_usage.count + excluded.count, updated_at = MAX(command_usage.updated_at, excluded.updated_at)"
  ).run(currentId, legacy.count || 0, legacy.updated_at || Date.now());
  db.prepare("DELETE FROM command_usage WHERE command_id = ?").run(legacyId);
}

function tableExists(db, name) {
  return Boolean(
    db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)
  );
}

function quoteIdentifier(name) {
  return `"${name.replace(/"/g, '""')}"`;
}

function mountLegacyRedirect(web, legacyId, currentId) {
  const legacyMount = `/plugins/${legacyId}`;
  const router = web.createRouter();
  router.use((req, res) => {
    const suffix = req.originalUrl.startsWith(legacyMount)
      ? req.originalUrl.slice(legacyMount.length)
      : "";
    res.redirect(308, `/plugins/${currentId}${suffix || ""}`);
  });
  web.mount(legacyMount, router);
}

function ensureDefaults(db) {
  const existing = getPluginSettings(db);
  for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) {
    if (existing[key] === undefined) {
      setPluginSetting(db, key, value);
    }
  }
}

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 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 parseNumber(value, fallback) {
  if (value === undefined || value === null || value === "") {
    return fallback;
  }
  const number = Number(value);
  if (!Number.isFinite(number)) {
    return fallback;
  }
  return number;
}

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

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

function getConfig(db) {
  const now = Date.now();
  if (cachedConfig && now - cachedConfigAt < 2000) {
    return cachedConfig;
  }
  const settings = getPluginSettings(db);
  const config = {
    hotpotato: {
      enabled: parseBoolean(settings.hotpotato_enabled, true),
      name: settings.hotpotato_name || "Hot Potato",
      trigger: normalizeCommand(settings.hotpotato_trigger, "hotpotato"),
      aliases: parseList(settings.hotpotato_aliases),
      minCost: parseNumber(settings.hotpotato_min_cost, 10),
      maxCost: parseNumber(settings.hotpotato_max_cost, 250),
      tossMin: parseNumber(settings.hotpotato_toss_min, 10),
      tossMax: parseNumber(settings.hotpotato_toss_max, 25),
      lossMultiplier: parseNumber(settings.hotpotato_loss_multiplier, 1),
      lossAdditive: parseNumber(settings.hotpotato_loss_additive, 0),
      presenceWindow: parseNumber(settings.hotpotato_presence_window, 300),
      platforms: {
        discord: parseBoolean(settings.hotpotato_platform_discord, true),
        twitch: parseBoolean(settings.hotpotato_platform_twitch, true),
        youtube: parseBoolean(settings.hotpotato_platform_youtube, true)
      }
    },
    coinflip: {
      enabled: parseBoolean(settings.coinflip_enabled, true),
      name: settings.coinflip_name || "Coinflip",
      trigger: normalizeCommand(settings.coinflip_trigger, "coinflip"),
      aliases: parseList(settings.coinflip_aliases),
      minBet: parseNumber(settings.coinflip_min_bet, 10),
      maxBet: parseNumber(settings.coinflip_max_bet, 500),
      multiplier: parseNumber(settings.coinflip_multiplier, 2),
      cooldown: parseNumber(settings.coinflip_cooldown, 10),
      platforms: {
        discord: parseBoolean(settings.coinflip_platform_discord, true),
        twitch: parseBoolean(settings.coinflip_platform_twitch, true),
        youtube: parseBoolean(settings.coinflip_platform_youtube, true)
      }
    },
    mystery: {
      enabled: parseBoolean(settings.mystery_enabled, true),
      name: settings.mystery_name || "Mystery Box",
      trigger: normalizeCommand(settings.mystery_trigger, "mysterybox"),
      aliases: parseList(settings.mystery_aliases),
      minBet: parseNumber(settings.mystery_min_bet, 10),
      maxBet: parseNumber(settings.mystery_max_bet, 500),
      multiplier: parseNumber(settings.mystery_multiplier, 2),
      cooldown: parseNumber(settings.mystery_cooldown, 10),
      platforms: {
        discord: parseBoolean(settings.mystery_platform_discord, true),
        twitch: parseBoolean(settings.mystery_platform_twitch, true),
        youtube: parseBoolean(settings.mystery_platform_youtube, true)
      }
    }
  };
  cachedConfig = config;
  cachedConfigAt = now;
  return config;
}

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

function buildResponses(db) {
  const settings = getPluginSettings(db);
  const custom = loadCustomResponses(db);
  const responses = {};
  for (const [key, list] of Object.entries(DEFAULT_RESPONSES)) {
    const override = Array.isArray(custom[key]) ? custom[key] : [];
    responses[key] = {
      key,
      label: toLabel(key),
      lines: override.length ? override : list
    };
  }
  return responses;
}

function getResponseLines(db, key) {
  const custom = loadCustomResponses(db);
  if (Array.isArray(custom[key]) && custom[key].length) {
    return custom[key];
  }
  return DEFAULT_RESPONSES[key] || [];
}

function loadCustomResponses(db) {
  const settings = getPluginSettings(db);
  let custom = {};
  try {
    custom = JSON.parse(settings.responses_json || "{}");
  } catch {
    custom = {};
  }
  return custom || {};
}

function ensureStatsTable(db) {
  migrateTable(db, `${LEGACY_STEM}_game_stats`, "economy_game_stats");
  db.prepare(
    "CREATE TABLE IF NOT EXISTS economy_game_stats (" +
      "game_key TEXT PRIMARY KEY," +
      "plays INTEGER NOT NULL DEFAULT 0," +
      "coins_won INTEGER NOT NULL DEFAULT 0," +
      "coins_lost INTEGER NOT NULL DEFAULT 0," +
      "last_played_at INTEGER," +
      "last_played_user_id TEXT," +
      "last_played_username TEXT" +
      ")"
  ).run();
}

function getGameStats(db, gameKey) {
  return db
    .prepare(
      "SELECT game_key, plays, coins_won, coins_lost, last_played_at, last_played_user_id, last_played_username " +
        "FROM economy_game_stats WHERE game_key = ?"
    )
    .get(gameKey);
}

function getGameStatsView(db, gameKey) {
  const stats = getGameStats(db, gameKey);
  if (!stats) {
    return {
      plays: 0,
      coinsWon: 0,
      coinsLost: 0,
      lastPlayedLabel: "Never",
      lastPlayedUser: null
    };
  }
  return {
    plays: stats.plays || 0,
    coinsWon: stats.coins_won || 0,
    coinsLost: stats.coins_lost || 0,
    lastPlayedLabel: stats.last_played_at
      ? new Date(stats.last_played_at).toLocaleString()
      : "Never",
    lastPlayedUser: stats.last_played_username || null
  };
}

function recordGamePlay(db, gameKey, { userId, username }) {
  const now = Date.now();
  db.prepare(
    "INSERT INTO economy_game_stats " +
      "(game_key, plays, coins_won, coins_lost, last_played_at, last_played_user_id, last_played_username) " +
      "VALUES (?, 1, 0, 0, ?, ?, ?) " +
      "ON CONFLICT(game_key) DO UPDATE SET " +
      "plays = plays + 1, " +
      "last_played_at = excluded.last_played_at, " +
      "last_played_user_id = excluded.last_played_user_id, " +
      "last_played_username = excluded.last_played_username"
  ).run(gameKey, now, userId || null, username || null);
}

function recordGameTotals(db, gameKey, { coinsWon = 0, coinsLost = 0 }) {
  const won = Math.floor(coinsWon || 0);
  const lost = Math.floor(coinsLost || 0);
  if (!won && !lost) {
    return;
  }
  db.prepare(
    "INSERT INTO economy_game_stats (game_key, plays, coins_won, coins_lost) " +
      "VALUES (?, 0, ?, ?) " +
      "ON CONFLICT(game_key) DO UPDATE SET " +
      "coins_won = coins_won + ?, " +
      "coins_lost = coins_lost + ?"
  ).run(gameKey, won, lost, won, lost);
}

function pickReply(lines) {
  if (!lines || !lines.length) {
    return "";
  }
  return lines[Math.floor(Math.random() * lines.length)];
}

function render(template, tokens) {
  return (template || "").replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
    if (Object.prototype.hasOwnProperty.call(tokens, key)) {
      return tokens[key];
    }
    return `{${key}}`;
  });
}

function replyWith(db, ctx, key, tokens) {
  const line = pickReply(getResponseLines(db, key));
  if (!line) {
    return null;
  }
  return render(line, tokens);
}

function toLabel(key) {
  return key
    .replace(/_/g, " ")
    .replace(/\b\w/g, (match) => match.toUpperCase());
}

function getFramework() {
  return global.lumiFrameworks?.economy || global.lumiFrameworks?.[LEGACY_STEM] || null;
}

function registerCommands({ db, commandRouter, settings }) {
  if (!commandRouter) {
    return null;
  }
  const rebuild = () => {
    const config = getConfig(db);
    const commands = [];

    if (config.hotpotato.enabled && config.hotpotato.trigger) {
      const triggers = [config.hotpotato.trigger, ...config.hotpotato.aliases];
      const platforms = platformsFromConfig(config.hotpotato.platforms);
      if (platforms.length && triggers.length) {
        commands.push({
          id: "economy-games:hotpotato",
          triggers,
          platforms,
          handler: (ctx) => handleHotPotato({ ctx, db, settings })
        });
      }
    }

    if (config.coinflip.enabled && config.coinflip.trigger) {
      const triggers = [config.coinflip.trigger, ...config.coinflip.aliases];
      const platforms = platformsFromConfig(config.coinflip.platforms);
      if (platforms.length && triggers.length) {
        commands.push({
          id: "economy-games:coinflip",
          triggers,
          platforms,
          handler: (ctx) => handleCoinflip({ ctx, db, settings })
        });
      }
    }

    if (config.mystery.enabled && config.mystery.trigger) {
      const triggers = [config.mystery.trigger, ...config.mystery.aliases];
      const platforms = platformsFromConfig(config.mystery.platforms);
      if (platforms.length && triggers.length) {
        commands.push({
          id: "economy-games:mystery",
          triggers,
          platforms,
          handler: (ctx) => handleMystery({ ctx, db, settings })
        });
      }
    }

    commandRouter.registerCommands(PLUGIN_ID, commands);
  };
  rebuild();
  return rebuild;
}

function platformsFromConfig(platforms) {
  return Object.entries(platforms || {})
    .filter(([, enabled]) => enabled)
    .map(([platform]) => platform);
}

function getChannelKey(ctx) {
  if (ctx.platform === "discord") {
    return ctx.meta?.message?.channelId || "discord";
  }
  if (ctx.platform === "twitch") {
    return ctx.meta?.channel || "twitch";
  }
  if (ctx.platform === "youtube") {
    return ctx.meta?.liveChatId || "youtube";
  }
  return "default";
}

function recordPresence(platform, channelKey, userId, name) {
  if (!platform || !channelKey || !userId) {
    return;
  }
  const channelMap = presence[platform] || new Map();
  if (!presence[platform]) {
    presence[platform] = channelMap;
  }
  const users = channelMap.get(channelKey) || new Map();
  users.set(userId, { name: name || "User", lastSeen: Date.now() });
  channelMap.set(channelKey, users);
}

function getActiveUsers(platform, channelKey, windowSeconds) {
  const channelMap = presence[platform];
  if (!channelMap) {
    return [];
  }
  const users = channelMap.get(channelKey);
  if (!users) {
    return [];
  }
  const now = Date.now();
  const cutoff = now - windowSeconds * 1000;
  const list = [];
  for (const [userId, info] of users.entries()) {
    if (info.lastSeen < cutoff) {
      users.delete(userId);
      continue;
    }
    list.push({ id: userId, name: info.name || "User" });
  }
  return list;
}

function attachDiscordPresence({ discordClient }) {
  if (!discordClient) {
    return;
  }
  discordClient.on("messageCreate", (message) => {
    if (!message || message.author?.bot) {
      return;
    }
    const display =
      message.member?.displayName ||
      message.author.globalName ||
      message.author.username ||
      message.author.tag;
    const profile = ensureUserForIdentity({
      provider: "discord",
      providerUserId: message.author.id,
      displayName: display,
      avatar: message.author.avatar
        ? `https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`
        : null
    });
    recordPresence("discord", message.channelId, profile.id, display);
  });
}

function attachTwitchPresence({ twitchClient }) {
  if (!twitchClient) {
    return;
  }
  twitchClient.on("message", (channel, tags, _message, self) => {
    if (self) {
      return;
    }
    const userId = tags["user-id"];
    if (!userId) {
      return;
    }
    const display = tags["display-name"] || tags.username || "Twitch User";
    const profile = ensureUserForIdentity({
      provider: "twitch",
      providerUserId: userId,
      displayName: display
    });
    recordPresence("twitch", channel, profile.id, display);
  });
}

function recordPresenceFromCtx(ctx) {
  const platform = ctx.platform;
  const channelKey = getChannelKey(ctx);
  const name = ctx.user.displayName || ctx.user.username || "User";
  recordPresence(platform, channelKey, ctx.user.id, name);
}

function randomBetween(min, max) {
  const low = Math.min(min, max);
  const high = Math.max(min, max);
  return Math.floor(Math.random() * (high - low + 1)) + low;
}

function parseAmount(value) {
  const number = Number(value);
  if (!Number.isFinite(number)) {
    return NaN;
  }
  if (number <= 0) {
    return NaN;
  }
  return Math.floor(number);
}

function getCooldownKey(ctx, key) {
  return `${ctx.platform}:${ctx.user.id}:${key}`;
}

function getCooldownLeft(ctx, key, cooldownSeconds) {
  const lookup = getCooldownKey(ctx, key);
  const last = cooldowns.get(lookup) || 0;
  const diff = cooldownSeconds * 1000 - (Date.now() - last);
  return diff > 0 ? Math.ceil(diff / 1000) : 0;
}

function setCooldown(ctx, key) {
  cooldowns.set(getCooldownKey(ctx, key), Date.now());
}

async function handleHotPotato({ ctx, db }) {
  recordPresenceFromCtx(ctx);
  const config = getConfig(db);
  const framework = getFramework();
  if (!framework) {
    await ctx.reply("Economy framework is not available.");
    return true;
  }
  if (!config.hotpotato.enabled || !config.hotpotato.platforms[ctx.platform]) {
    return false;
  }

  const channelKey = getChannelKey(ctx);
  const gameKey = `${ctx.platform}:${channelKey}`;
  const current = hotPotatoGames.get(gameKey);
  const sub = (ctx.args[0] || "").toLowerCase();
  const isToss = ["toss", "pass", "throw", "retoss"].includes(sub);

  if (isToss) {
    if (!current) {
      const msg = replyWith(db, ctx, "hotpotato_not_active", {
        game: config.hotpotato.name,
        trigger: config.hotpotato.trigger
      });
      if (msg) {
        await ctx.reply(msg);
      }
      return true;
    }
    if (current.holderId !== ctx.user.id) {
      const msg = replyWith(db, ctx, "hotpotato_not_holder", {});
      if (msg) {
        await ctx.reply(msg);
      }
      return true;
    }
    const next = pickRandomUser({
      platform: ctx.platform,
      channelKey,
      exclude: [ctx.user.id],
      windowSeconds: config.hotpotato.presenceWindow
    });
    if (!next) {
      const msg = replyWith(db, ctx, "hotpotato_no_targets", {});
      if (msg) {
        await ctx.reply(msg);
      }
      return true;
    }
    touchUser(current, ctx.user.id, ctx.user.displayName || ctx.user.username);
    touchUser(current, next.id, next.name);
    current.holderId = next.id;
    current.holderName = next.name;
    current.reply = ctx.reply;
    resetHotPotatoTimer(gameKey, current, config);
    const msg = replyWith(db, ctx, "hotpotato_toss", {
      user: ctx.user.displayName || ctx.user.username,
      target: next.name,
      seconds: current.seconds
    });
    if (msg) {
      await ctx.reply(msg);
    }
    return true;
  }

  if (current) {
    const msg = replyWith(db, ctx, "hotpotato_already_active", {
      game: config.hotpotato.name,
      holder: current.holderName || "Someone"
    });
    if (msg) {
      await ctx.reply(msg);
    }
    return true;
  }

  const amount = parseAmount(ctx.args[0]);
  if (!Number.isFinite(amount) ||
      amount < config.hotpotato.minCost ||
      amount > config.hotpotato.maxCost) {
    const msg = replyWith(db, ctx, "hotpotato_invalid_amount", {
      min: config.hotpotato.minCost,
      max: config.hotpotato.maxCost
    });
    if (msg) {
      await ctx.reply(msg);
    }
    return true;
  }

  const stakeResult = framework.removeBalance({
    userId: ctx.user.id,
    amount,
    note: `${config.hotpotato.name} entry`
  });
  if (stakeResult?.ok === false) {
    const msg = replyWith(db, ctx, "coinflip_insufficient", {
      reason: stakeResult.message || "Insufficient balance."
    });
    if (msg) {
      await ctx.reply(msg);
    }
    return true;
  }

  const target = pickRandomUser({
    platform: ctx.platform,
    channelKey,
    exclude: [ctx.user.id],
    windowSeconds: config.hotpotato.presenceWindow
  });
  if (!target) {
    const msg = replyWith(db, ctx, "hotpotato_no_targets", {});
    if (msg) {
      await ctx.reply(msg);
    }
    return true;
  }

  const displayName = ctx.user.displayName || ctx.user.username || "User";
  recordGamePlay(db, "hotpotato", { userId: ctx.user.id, username: displayName });
  recordGameTotals(db, "hotpotato", { coinsLost: amount });

  const state = {
    id: crypto.randomUUID(),
    amount,
    holderId: target.id,
    holderName: target.name,
    touched: new Map(),
    reply: ctx.reply,
    platform: ctx.platform,
    channelKey,
    db
  };
  touchUser(state, ctx.user.id, displayName);
  touchUser(state, target.id, target.name);
  hotPotatoGames.set(gameKey, state);
  resetHotPotatoTimer(gameKey, state, config);

  const msg = replyWith(db, ctx, "hotpotato_start", {
    user: ctx.user.displayName || ctx.user.username,
    target: target.name,
    amount,
    game: config.hotpotato.name,
    seconds: state.seconds
  });
  if (msg) {
    await ctx.reply(msg);
  }
  return true;
}

function resetHotPotatoTimer(gameKey, state, config) {
  if (state.timer) {
    clearTimeout(state.timer);
  }
  const seconds = randomBetween(config.hotpotato.tossMin, config.hotpotato.tossMax);
  state.seconds = seconds;
  state.deadlineAt = Date.now() + seconds * 1000;
  state.timer = setTimeout(() => resolveHotPotato(gameKey, state.id), seconds * 1000);
}

function touchUser(state, userId, name) {
  if (!state.touched.has(userId)) {
    state.touched.set(userId, name || "User");
  }
}

function pickRandomUser({ platform, channelKey, exclude, windowSeconds }) {
  const list = getActiveUsers(platform, channelKey, windowSeconds).filter(
    (user) => !exclude.includes(user.id)
  );
  if (!list.length) {
    return null;
  }
  return list[Math.floor(Math.random() * list.length)];
}

async function resolveHotPotato(gameKey, gameId) {
  const state = hotPotatoGames.get(gameKey);
  if (!state || state.id !== gameId) {
    return;
  }
  const framework = getFramework();
  if (!framework) {
    hotPotatoGames.delete(gameKey);
    return;
  }
  const config = getConfig(state.db);
  const penaltyBase = state.amount || 0;
  const lossTotal = Math.max(
    0,
    Math.floor(penaltyBase * config.hotpotato.lossMultiplier + config.hotpotato.lossAdditive)
  );
  const recipients = Array.from(state.touched.entries()).filter(
    ([id]) => id !== state.holderId
  );
  const perRecipient =
    recipients.length && lossTotal > 0 ? Math.floor(lossTotal / recipients.length) : 0;
  const winners = [];
  if (perRecipient > 0 && recipients.length) {
    for (const [userId, name] of recipients) {
      try {
        framework.createTransaction({
          type: "hotpotato_payout",
          amount: perRecipient,
          fromUserId: state.holderId,
          toUserId: userId,
          note: "Hot potato penalty",
          meta: { game: "hotpotato" },
          allowNegative: true
        });
        winners.push(name);
      } catch {
        // ignore individual payout failures
      }
    }
    recordGameTotals(state.db, "hotpotato", {
      coinsLost: lossTotal,
      coinsWon: perRecipient * recipients.length
    });
  } else if (lossTotal > 0) {
    try {
      framework.createTransaction({
        type: "hotpotato_penalty",
        amount: lossTotal,
        fromUserId: state.holderId,
        toUserId: null,
        note: "Hot potato penalty",
        meta: { game: "hotpotato" },
        allowNegative: true
      });
    } catch {
      // ignore
    }
    recordGameTotals(state.db, "hotpotato", { coinsLost: lossTotal });
  }

  const msg = replyWith(state.db, { reply: state.reply }, "hotpotato_timeout", {
    loser: state.holderName || "Someone",
    loss: lossTotal,
    winners: winners.length ? winners.join(", ") : "No one"
  });
  if (msg && state.reply) {
    try {
      await state.reply(msg);
    } catch {
      // ignore
    }
  }
  hotPotatoGames.delete(gameKey);
}

async function handleCoinflip({ ctx, db }) {
  recordPresenceFromCtx(ctx);
  const config = getConfig(db);
  const framework = getFramework();
  if (!framework) {
    await ctx.reply("Economy framework is not available.");
    return true;
  }
  if (!config.coinflip.enabled || !config.coinflip.platforms[ctx.platform]) {
    return false;
  }

  const amount = parseAmount(ctx.args[0]);
  if (!Number.isFinite(amount) || amount < config.coinflip.minBet || amount > config.coinflip.maxBet) {
    const msg = replyWith(db, ctx, "coinflip_invalid", {
      min: config.coinflip.minBet,
      max: config.coinflip.maxBet
    });
    if (msg) {
      await ctx.reply(msg);
    }
    return true;
  }

  const cooldownLeft = getCooldownLeft(ctx, "coinflip", config.coinflip.cooldown);
  if (cooldownLeft > 0) {
    const msg = replyWith(db, ctx, "coinflip_cooldown", {
      seconds: cooldownLeft
    });
    if (msg) {
      await ctx.reply(msg);
    }
    return true;
  }

  const stakeResult = framework.removeBalance({
    userId: ctx.user.id,
    amount,
    note: `${config.coinflip.name} bet`
  });
  if (stakeResult?.ok === false) {
    const msg = replyWith(db, ctx, "coinflip_insufficient", {
      reason: stakeResult.message || "Insufficient balance."
    });
    if (msg) {
      await ctx.reply(msg);
    }
    return true;
  }

  setCooldown(ctx, "coinflip");
  const displayName = ctx.user.displayName || ctx.user.username || "User";
  recordGamePlay(db, "coinflip", { userId: ctx.user.id, username: displayName });
  const win = Math.random() >= 0.5;
  if (win) {
    const payout = Math.floor(amount * config.coinflip.multiplier);
    framework.addBalance({
      userId: ctx.user.id,
      amount: payout,
      note: `${config.coinflip.name} win`
    });
    recordGameTotals(db, "coinflip", { coinsLost: amount, coinsWon: payout });
    const msg = replyWith(db, ctx, "coinflip_win", {
      user: displayName,
      payout
    });
    if (msg) {
      await ctx.reply(msg);
    }
    return true;
  }

  recordGameTotals(db, "coinflip", { coinsLost: amount, coinsWon: 0 });
  const msg = replyWith(db, ctx, "coinflip_lose", {
    user: displayName,
    amount
  });
  if (msg) {
    await ctx.reply(msg);
  }
  return true;
}

async function handleMystery({ ctx, db }) {
  recordPresenceFromCtx(ctx);
  const config = getConfig(db);
  const framework = getFramework();
  if (!framework) {
    await ctx.reply("Economy framework is not available.");
    return true;
  }
  if (!config.mystery.enabled || !config.mystery.platforms[ctx.platform]) {
    return false;
  }

  const amount = parseAmount(ctx.args[0]);
  if (!Number.isFinite(amount) || amount < config.mystery.minBet || amount > config.mystery.maxBet) {
    const msg = replyWith(db, ctx, "mystery_invalid", {
      min: config.mystery.minBet,
      max: config.mystery.maxBet
    });
    if (msg) {
      await ctx.reply(msg);
    }
    return true;
  }

  const cooldownLeft = getCooldownLeft(ctx, "mystery", config.mystery.cooldown);
  if (cooldownLeft > 0) {
    const msg = replyWith(db, ctx, "mystery_cooldown", {
      seconds: cooldownLeft
    });
    if (msg) {
      await ctx.reply(msg);
    }
    return true;
  }

  const stakeResult = framework.removeBalance({
    userId: ctx.user.id,
    amount,
    note: `${config.mystery.name} entry`
  });
  if (stakeResult?.ok === false) {
    const msg = replyWith(db, ctx, "mystery_insufficient", {
      reason: stakeResult.message || "Insufficient balance."
    });
    if (msg) {
      await ctx.reply(msg);
    }
    return true;
  }

  setCooldown(ctx, "mystery");
  const displayName = ctx.user.displayName || ctx.user.username || "User";
  const maxPayout = Math.max(0, Math.floor(amount * config.mystery.multiplier));
  const payout = Math.floor(Math.random() * (maxPayout + 1));
  if (payout > 0) {
    framework.addBalance({
      userId: ctx.user.id,
      amount: payout,
      note: `${config.mystery.name} payout`
    });
  }
  recordGamePlay(db, "mystery", { userId: ctx.user.id, username: displayName });
  recordGameTotals(db, "mystery", { coinsLost: amount, coinsWon: payout });
  const msg = replyWith(db, ctx, "mystery_result", {
    user: displayName,
    amount,
    payout
  });
  if (msg) {
    await ctx.reply(msg);
  }
  return true;
}
