
const path = require("path");
const fs = require("fs");
const crypto = require("crypto");
const express = require("express");
const multer = require("multer");
const EventEmitter = require("events");
const { ensureUserForIdentity } = require("../../src/services/users");
const {
  cleanupUploadedFiles,
  validateUploadedFile
} = require("../../src/services/upload-security");

const PLUGIN_ID = "economy-framework";
const LEGACY_STEM = ["echo", "nomy"].join("");
const LEGACY_PLUGIN_ID = `${LEGACY_STEM}-framework`;
const LEGACY_FRAMEWORK_KEY = LEGACY_STEM;
const LEGACY_NAME_PROVIDER = `${LEGACY_STEM}_name`;
const DEFAULT_SETTINGS = {
  currency_name: "Coin",
  currency_name_plural: "Coins",
  currency_icon_path: "",
  command_root: "coins",
  command_aliases: "",
  banking_label: "Banking",
  banking_enabled: "1",
  community_fund_name: "Community fund",
  community_fund_name_plural: "Community funds",
  platform_discord: "1",
  platform_twitch: "1",
  platform_youtube: "1",
  transfer_cooldown_seconds: "10",
  earn_discord_message_enabled: "1",
  earn_discord_message_amount: "1",
  earn_discord_message_cooldown: "30",
  earn_twitch_message_enabled: "1",
  earn_twitch_message_amount: "1",
  earn_twitch_message_cooldown: "30",
  earn_discord_voice_enabled: "0",
  earn_discord_voice_amount_per_min: "2",
  earn_discord_voice_tick_minutes: "1",
  tier_discord_booster_multiplier: "1.25",
  tier_twitch_sub_multiplier: "1.5",
  tier_twitch_mod_multiplier: "1.2",
  tier_twitch_vip_multiplier: "1.1",
  tier_twitch_broadcaster_multiplier: "2.0",
  custom_events: "[]",
  response_templates: ""
};

const DEFAULT_RESPONSES = {
  balance_self: {
    label: "Balance (self)",
    mode: "random",
    replies: [
      { text: "Your balance is {balance_text}.", weight: 1 },
      { text: "You have {balance_text} available.", weight: 1 }
    ]
  },
  top_list: {
    label: "Top balances",
    mode: "random",
    replies: [{ text: "Top balances: {lines}", weight: 1 }]
  },
  top_empty: {
    label: "Top balances (empty)",
    mode: "random",
    replies: [{ text: "No balances yet.", weight: 1 }]
  },
  stats: {
    label: "Global stats",
    mode: "random",
    replies: [
      {
        text: "Total in circulation: {total_balance_text}. Total spent: {total_spent_text}.",
        weight: 1
      }
    ]
  },
  pay_success: {
    label: "Pay success",
    mode: "random",
    replies: [
      { text: "Sent {amount_text} to {target}.", weight: 1 },
      { text: "Transfer complete: {target} received {amount_text}.", weight: 1 }
    ]
  },
  pay_missing: {
    label: "Pay missing arguments",
    mode: "random",
    replies: [{ text: "Usage: {usage}", weight: 1 }]
  },
  pay_cooldown: {
    label: "Pay cooldown",
    mode: "random",
    replies: [{ text: "Please wait {cooldown}s before sending again.", weight: 1 }]
  },
  pay_self: {
    label: "Pay self",
    mode: "random",
    replies: [{ text: "You cannot pay yourself.", weight: 1 }]
  },
  pay_not_found: {
    label: "Pay user not found",
    mode: "random",
    replies: [{ text: "I couldn't find that user.", weight: 1 }]
  },
  pay_insufficient: {
    label: "Pay insufficient balance",
    mode: "random",
    replies: [{ text: "{reason}", weight: 1 }]
  },
  grant_success: {
    label: "Grant success",
    mode: "random",
    replies: [{ text: "Granted {amount_text} to {target}.", weight: 1 }]
  },
  take_success: {
    label: "Take success",
    mode: "random",
    replies: [{ text: "Removed {amount_text} from {target}.", weight: 1 }]
  },
  funds_list: {
    label: "Community funds list",
    mode: "random",
    replies: [{ text: "{funds_label}: {lines}", weight: 1 }]
  },
  funds_empty: {
    label: "Community funds (empty)",
    mode: "random",
    replies: [{ text: "No {funds_label} are active yet.", weight: 1 }]
  },
  fund_missing: {
    label: "Fund missing arguments",
    mode: "random",
    replies: [{ text: "Usage: {usage}", weight: 1 }]
  },
  fund_not_found: {
    label: "Fund not found",
    mode: "random",
    replies: [{ text: "That {fund_label} is not active.", weight: 1 }]
  },
  fund_donate_success: {
    label: "Fund donation success",
    mode: "random",
    replies: [
      { text: "Donated {amount_text} to {fund}.", weight: 1 },
      { text: "Thanks! {amount_text} added to {fund}.", weight: 1 }
    ]
  },
  permission_denied: {
    label: "Permission denied",
    mode: "random",
    replies: [{ text: "You do not have permission to do that.", weight: 1 }]
  },
  reward_missing: {
    label: "Event reward missing arguments",
    mode: "random",
    replies: [{ text: "Usage: {usage}", weight: 1 }]
  },
  reward_not_found: {
    label: "Event reward not found",
    mode: "random",
    replies: [{ text: "That event is not configured.", weight: 1 }]
  },
  reward_success: {
    label: "Event reward success",
    mode: "random",
    replies: [{ text: "Awarded {amount_text} to {target}.", weight: 1 }]
  },
  help: {
    label: "Help",
    mode: "random",
    replies: [{ text: "{help}", weight: 1 }]
  }
};

const emitter = new EventEmitter();
const messageCooldowns = new Map();
const transferCooldowns = new Map();
const voiceStates = new Map();
let voiceTimer = null;
let activityFlushTimer = null;
let cachedConfig = null;
let cachedConfigAt = 0;
let settingsApi = null;
const ACTIVITY_REWARD_NOTE = "Activity Reward";
const ACTIVITY_REWARD_SOURCES = {
  discord_message: "Discord Message",
  twitch_message: "Twitch Message",
  discord_voice: "Discord Voice"
};

module.exports = {
  id: PLUGIN_ID,
  init({
    app,
    web,
    settings,
    db,
    commandRouter,
    discordClient,
    twitchClient
  }) {
    settingsApi = settings;
    const repoRoot = path.join(__dirname, "..", "..");
    migrateLegacyInstall(db, repoRoot);
    ensureTables(db);
    cleanupFallbackUserArtifacts(db);
    ensureDefaults(db);
    startActivityRewardFlusher(db);

    const api = buildApi({ db });
    registerFramework(api);
    const refreshCommands = registerCommands({ db, settings, commandRouter });

    attachDiscordListeners({ db, settings, discordClient });
    attachTwitchListeners({ db, settings, twitchClient });
    installProfileHook(app, () => getConfig(db));

    const uploadDir = path.join(repoRoot, "data", "economy-framework");
    fs.mkdirSync(uploadDir, { recursive: true });
    const upload = multer({
      dest: uploadDir,
      limits: { fileSize: 2 * 1024 * 1024, files: 1, fields: 10, parts: 12 },
      fileFilter: (_req, file, cb) => {
        if (file.mimetype === "image/png") {
          return cb(null, true);
        }
        cb(new Error("Only PNG files are allowed."));
      }
    });
    const currencyIconUpload = (req, res, next) => upload.single("currency_icon")(req, res, (error) => {
      if (error) {
        cleanupUploadedFiles(req.file);
        req.file = null;
        req.currencyIconUploadError = error.code === "LIMIT_FILE_SIZE"
          ? "Currency icons must be 2 MB or smaller."
          : error.message;
        return next();
      }
      try {
        if (req.file) {
          validateUploadedFile(req.file, {
            allowedTypes: ["png"],
            maxBytes: 2 * 1024 * 1024,
            requireExtension: true,
            label: "currency icon"
          });
        }
      } catch (validationError) {
        cleanupUploadedFiles(req.file);
        req.file = null;
        req.currencyIconUploadError = validationError.message;
      }
      next();
    });

    const router = web.createRouter();
    router.use("/assets", express.static(uploadDir));

    router.get("/", (req, res) => {
      const config = getConfig(db);
      const user = req.session.user || null;
      const isAdmin = Boolean(user?.isAdmin);
      const isMod = Boolean(user?.isAdmin || user?.isMod);
      const userBalance = user ? getBalance(db, user.id) : 0;
      const transactions = listTransactions(db, {
        userId: isAdmin ? null : user?.id,
        limit: 1000
      });
      const globalStats = buildGlobalStats(db);
      const topBalances = listTopBalances(db, 10);
      const funds = listFunds(db);
      const events = getCustomEvents(config);
      const responses = Object.values(config.responses || {});

      res.render(path.join(__dirname, "views", "economy.ejs"), {
        title: "Economy Framework",
        config,
        user,
        isAdmin,
        isMod,
        userBalance,
        transactions,
        globalStats,
        topBalances,
        funds,
        events,
        responses
      });
    });

    router.post("/settings/currency", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      setPluginSetting(db, "currency_name", (req.body.currency_name || "").trim());
      setPluginSetting(
        db,
        "currency_name_plural",
        (req.body.currency_name_plural || "").trim()
      );
      setPluginSetting(db, "command_root", (req.body.command_root || "").trim());
      setPluginSetting(db, "command_aliases", (req.body.command_aliases || "").trim());
      invalidateConfigCache();
      if (refreshCommands) {
        refreshCommands();
      }
      req.session.flash = {
        type: "success",
        message: "Currency settings updated."
      };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/settings/platforms", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      setPluginSetting(db, "platform_discord", req.body.platform_discord ? "1" : "0");
      setPluginSetting(db, "platform_twitch", req.body.platform_twitch ? "1" : "0");
      setPluginSetting(db, "platform_youtube", req.body.platform_youtube ? "1" : "0");
      invalidateConfigCache();
      if (refreshCommands) {
        refreshCommands();
      }
      req.session.flash = {
        type: "success",
        message: "Platform settings updated."
      };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/settings/earn", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      setPluginSetting(
        db,
        "earn_discord_message_enabled",
        req.body.earn_discord_message_enabled ? "1" : "0"
      );
      setPluginSetting(
        db,
        "earn_discord_message_amount",
        (req.body.earn_discord_message_amount || "0").trim()
      );
      setPluginSetting(
        db,
        "earn_discord_message_cooldown",
        (req.body.earn_discord_message_cooldown || "0").trim()
      );
      setPluginSetting(
        db,
        "earn_twitch_message_enabled",
        req.body.earn_twitch_message_enabled ? "1" : "0"
      );
      setPluginSetting(
        db,
        "earn_twitch_message_amount",
        (req.body.earn_twitch_message_amount || "0").trim()
      );
      setPluginSetting(
        db,
        "earn_twitch_message_cooldown",
        (req.body.earn_twitch_message_cooldown || "0").trim()
      );
      setPluginSetting(
        db,
        "earn_discord_voice_enabled",
        req.body.earn_discord_voice_enabled ? "1" : "0"
      );
      setPluginSetting(
        db,
        "earn_discord_voice_amount_per_min",
        (req.body.earn_discord_voice_amount_per_min || "0").trim()
      );
      setPluginSetting(
        db,
        "earn_discord_voice_tick_minutes",
        (req.body.earn_discord_voice_tick_minutes || "1").trim()
      );
      invalidateConfigCache();
      req.session.flash = {
        type: "success",
        message: "Earning rules updated."
      };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/settings/tiers", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      setPluginSetting(
        db,
        "tier_discord_booster_multiplier",
        (req.body.tier_discord_booster_multiplier || "1").trim()
      );
      setPluginSetting(
        db,
        "tier_twitch_sub_multiplier",
        (req.body.tier_twitch_sub_multiplier || "1").trim()
      );
      setPluginSetting(
        db,
        "tier_twitch_mod_multiplier",
        (req.body.tier_twitch_mod_multiplier || "1").trim()
      );
      setPluginSetting(
        db,
        "tier_twitch_vip_multiplier",
        (req.body.tier_twitch_vip_multiplier || "1").trim()
      );
      setPluginSetting(
        db,
        "tier_twitch_broadcaster_multiplier",
        (req.body.tier_twitch_broadcaster_multiplier || "1").trim()
      );
      invalidateConfigCache();
      req.session.flash = {
        type: "success",
        message: "Tier multipliers updated."
      };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/settings/banking", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      setPluginSetting(db, "banking_label", (req.body.banking_label || "").trim());
      setPluginSetting(
        db,
        "banking_enabled",
        req.body.banking_enabled ? "1" : "0"
      );
      setPluginSetting(
        db,
        "community_fund_name",
        (req.body.community_fund_name || "").trim()
      );
      setPluginSetting(
        db,
        "community_fund_name_plural",
        (req.body.community_fund_name_plural || "").trim()
      );
      invalidateConfigCache();
      req.session.flash = {
        type: "success",
        message: "Banking labels updated."
      };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/settings/responses", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      const key = (req.body.response_key || "").trim();
      if (!key) {
        req.session.flash = {
          type: "error",
          message: "Response key is required."
        };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      const mode = (req.body.response_mode || "random").trim();
      const texts = Array.isArray(req.body.response_text)
        ? req.body.response_text
        : [req.body.response_text];
      const weights = Array.isArray(req.body.response_weight)
        ? req.body.response_weight
        : [req.body.response_weight];
      const replies = (texts || [])
        .map((text, index) => ({
          text: (text || "").trim(),
          weight: Number(weights?.[index] || 1)
        }))
        .filter((entry) => entry.text);
      const current = getResponseTemplates(db);
      current[key] = {
        ...current[key],
        mode: mode === "weighted" ? "weighted" : "random",
        replies: replies.length ? replies : current[key]?.replies || []
      };
      setPluginSetting(db, "response_templates", JSON.stringify(current));
      invalidateConfigCache();
      req.session.flash = {
        type: "success",
        message: "Responses updated."
      };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/settings/icon", (req, res, next) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      next();
    }, currencyIconUpload, (req, res) => {
      if (req.currencyIconUploadError) {
        req.session.flash = { type: "error", message: req.currencyIconUploadError };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      if (!req.file) {
        req.session.flash = { type: "error", message: "Upload a PNG icon." };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      const filename = `currency-${Date.now()}-${crypto.randomUUID()}.png`;
      const targetPath = path.join(uploadDir, filename);
      fs.renameSync(req.file.path, targetPath);
      const relativePath = `/plugins/${PLUGIN_ID}/assets/${filename}`;
      setPluginSetting(db, "currency_icon_path", relativePath);
      invalidateConfigCache();
      req.session.flash = {
        type: "success",
        message: "Currency icon updated."
      };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/accounts/adjust", (req, res) => {
      if (!req.session.user || !req.session.user.isMod) {
        return deny(res);
      }
      const targetUserId = (req.body.target_user_id || "").trim();
      const targetName = (req.body.username || "").trim();
      const amount = parseSignedAmount(req.body.amount);
      if ((!targetUserId && !targetName) || !Number.isFinite(amount)) {
        req.session.flash = {
          type: "error",
          message: "Username and amount are required."
        };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      const target = targetUserId
        ? db.prepare("SELECT id, internal_username FROM user_profiles WHERE id = ?").get(targetUserId)
        : findUserByInternalName(db, targetName);
      if (!target) {
        req.session.flash = { type: "error", message: "User not found." };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      const note = (req.body.note || "").trim();
      if (amount === 0) {
        req.session.flash = {
          type: "error",
          message: "Amount must be non-zero."
        };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      adjustBalance(db, {
        userId: target.id,
        amount,
        note,
        meta: {
          actorId: req.session.user.id,
          actorName: req.session.user.username
        }
      });
      req.session.flash = {
        type: "success",
        message: "Balance updated."
      };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/funds/create", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      const name = (req.body.name || "").trim();
      const description = (req.body.description || "").trim();
      const target = parseInt(req.body.target_amount || "0", 10);
      if (!name) {
        req.session.flash = { type: "error", message: "Fund name is required." };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      createFund(db, {
        name,
        description,
        targetAmount: Number.isFinite(target) ? target : 0
      });
      req.session.flash = { type: "success", message: "Fund created." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/funds/:id/update", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      updateFund(db, {
        id: req.params.id,
        name: (req.body.name || "").trim(),
        description: (req.body.description || "").trim(),
        targetAmount: parseInt(req.body.target_amount || "0", 10),
        status: req.body.status || "active"
      });
      req.session.flash = { type: "success", message: "Fund updated." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/events/create", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      const name = (req.body.name || "").trim();
      const amount = parseInt(req.body.amount || "0", 10);
      if (!name || !Number.isFinite(amount)) {
        req.session.flash = {
          type: "error",
          message: "Event name and amount are required."
        };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      const config = getConfig(db);
      const events = getCustomEvents(config);
      events.push({ id: crypto.randomUUID(), name, amount });
      setPluginSetting(db, "custom_events", JSON.stringify(events));
      invalidateConfigCache();
      req.session.flash = { type: "success", message: "Event added." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/events/:id/delete", (req, res) => {
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      const config = getConfig(db);
      const events = getCustomEvents(config).filter(
        (event) => event.id !== req.params.id
      );
      setPluginSetting(db, "custom_events", JSON.stringify(events));
      invalidateConfigCache();
      req.session.flash = { type: "success", message: "Event removed." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    const bankRouter = web.createRouter();
    bankRouter.use((req, res, next) => {
      if (!req.session.user) {
        return res.redirect("/");
      }
      const config = getConfig(db);
      if (!config.banking.enabled) {
        return res.redirect("/profile");
      }
      req.bankingConfig = config;
      next();
    });

    bankRouter.get("/", (req, res) => {
      const config = req.bankingConfig || getConfig(db);
      const user = req.session.user;
      const userStats = buildUserStats(db, user.id);
      const transactions = listTransactions(db, {
        userId: user.id,
        limit: 1000
      });
      const funds = listFunds(db).filter((fund) => fund.status === "active");
      res.render(path.join(__dirname, "views", "banking.ejs"), {
        title: config.banking.label,
        config,
        user,
        userStats,
        transactions,
        funds
      });
    });

    bankRouter.post("/transfer", (req, res) => {
      if (!req.session.user) {
        return res.redirect("/");
      }
      const config = req.bankingConfig || getConfig(db);
      const targetName = (req.body.username || "").trim();
      const amount = parseAmount(req.body.amount);
      const note = (req.body.note || "").trim();
      if ((!targetUserId && !targetName) || !Number.isFinite(amount)) {
        req.session.flash = {
          type: "error",
          message: "Recipient and amount are required."
        };
        return res.redirect("/profile/banking");
      }
      const cooldownLeft = getCooldownLeft(req.session.user.id, config);
      if (cooldownLeft > 0) {
        req.session.flash = {
          type: "error",
          message: `Please wait ${cooldownLeft}s before sending again.`
        };
        return res.redirect("/profile/banking");
      }
      const target = targetUserId
        ? db.prepare("SELECT id, internal_username FROM user_profiles WHERE id = ?").get(targetUserId)
        : findUserByInternalName(db, targetName.replace(/^@/, ""));
      if (!target) {
        req.session.flash = { type: "error", message: "User not found." };
        return res.redirect("/profile/banking");
      }
      if (target.id === req.session.user.id) {
        req.session.flash = {
          type: "error",
          message: "You cannot transfer funds to yourself."
        };
        return res.redirect("/profile/banking");
      }
      const success = transferBalance(db, {
        fromUserId: req.session.user.id,
        toUserId: target.id,
        amount,
        note,
        meta: { source: "banking_ui" }
      });
      if (!success.ok) {
        req.session.flash = { type: "error", message: success.message };
        return res.redirect("/profile/banking");
      }
      setCooldown(req.session.user.id);
      req.session.flash = { type: "success", message: "Transfer completed." };
      return res.redirect("/profile/banking");
    });

    bankRouter.post("/funds/:id/donate", (req, res) => {
      if (!req.session.user) {
        return res.redirect("/");
      }
      const config = req.bankingConfig || getConfig(db);
      const amount = parseAmount(req.body.amount);
      const note = (req.body.note || "").trim();
      if (!Number.isFinite(amount)) {
        req.session.flash = {
          type: "error",
          message: "Enter a valid amount."
        };
        return res.redirect("/profile/banking");
      }
      const cooldownLeft = getCooldownLeft(req.session.user.id, config);
      if (cooldownLeft > 0) {
        req.session.flash = {
          type: "error",
          message: `Please wait ${cooldownLeft}s before donating again.`
        };
        return res.redirect("/profile/banking");
      }
      const fund = db
        .prepare("SELECT * FROM economy_pots WHERE id = ?")
        .get(req.params.id);
      if (!fund || fund.status !== "active") {
        req.session.flash = {
          type: "error",
          message: "That fund is not active."
        };
        return res.redirect("/profile/banking");
      }
      const result = spendBalance(db, {
        userId: req.session.user.id,
        amount,
        note: note || `Donation to ${fund.name}`,
        meta: { fundId: fund.id, source: "banking_ui" }
      });
      if (!result.ok) {
        req.session.flash = { type: "error", message: result.message };
        return res.redirect("/profile/banking");
      }
      addFundContribution(db, fund.id, req.session.user.id, amount);
      setCooldown(req.session.user.id);
      req.session.flash = { type: "success", message: "Donation completed." };
      return res.redirect("/profile/banking");
    });

    web.mount(`/plugins/${PLUGIN_ID}`, router, {
      label: "Economy",
      role: "public",
      section: "plugins"
    });
    mountLegacyRedirect(web, LEGACY_PLUGIN_ID, PLUGIN_ID);
    web.mount("/profile/banking", bankRouter);
  }
};

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

function ensureTables(db) {
  db.exec(`
    CREATE TABLE IF NOT EXISTS economy_accounts (
      user_id TEXT PRIMARY KEY,
      balance INTEGER NOT NULL DEFAULT 0,
      updated_at INTEGER NOT NULL
    );

    CREATE TABLE IF NOT EXISTS economy_transactions (
      id TEXT PRIMARY KEY,
      type TEXT NOT NULL,
      amount INTEGER NOT NULL,
      from_user_id TEXT,
      to_user_id TEXT,
      note TEXT,
      meta TEXT,
      created_at INTEGER NOT NULL
    );

    CREATE TABLE IF NOT EXISTS economy_pots (
      id TEXT PRIMARY KEY,
      name TEXT NOT NULL UNIQUE,
      description TEXT,
      target_amount INTEGER NOT NULL DEFAULT 0,
      current_amount INTEGER NOT NULL DEFAULT 0,
      status TEXT NOT NULL DEFAULT 'active',
      created_at INTEGER NOT NULL,
      updated_at INTEGER NOT NULL
    );

    CREATE TABLE IF NOT EXISTS economy_pot_contributions (
      id TEXT PRIMARY KEY,
      pot_id TEXT NOT NULL,
      user_id TEXT NOT NULL,
      amount INTEGER NOT NULL,
      created_at INTEGER NOT NULL
    );

    CREATE TABLE IF NOT EXISTS economy_activity_reward_hourly (
      user_id TEXT NOT NULL,
      hour_start INTEGER NOT NULL,
      source TEXT NOT NULL,
      amount INTEGER NOT NULL DEFAULT 0,
      hits INTEGER NOT NULL DEFAULT 0,
      minutes INTEGER NOT NULL DEFAULT 0,
      PRIMARY KEY (user_id, hour_start, source)
    );

    CREATE INDEX IF NOT EXISTS economy_transactions_created_at_idx
      ON economy_transactions (created_at);

    CREATE INDEX IF NOT EXISTS economy_activity_reward_hourly_hour_idx
      ON economy_activity_reward_hourly (hour_start);
  `);
}

function migrateLegacyInstall(db, repoRoot) {
  migratePluginSettings(db);
  migrateLegacyTables(db);
  migrateLegacyNameProvider(db);
  migrateLegacyIconPath(db);
  migrateLegacyUploadDir(repoRoot);
  mergeCommandUsage(db, `${LEGACY_FRAMEWORK_KEY}:root`, "economy:root");
}

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 migrateLegacyTables(db) {
  [
    "accounts",
    "transactions",
    "pots",
    "pot_contributions",
    "activity_reward_hourly"
  ].forEach((suffix) => {
    migrateTable(db, `${LEGACY_STEM}_${suffix}`, `economy_${suffix}`);
  });
}

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 migrateLegacyNameProvider(db) {
  if (!tableExists(db, "linked_accounts")) {
    return;
  }
  db.prepare("UPDATE linked_accounts SET provider = ? WHERE provider = ?").run(
    "economy_name",
    LEGACY_NAME_PROVIDER
  );
}

function migrateLegacyIconPath(db) {
  const legacyPrefix = `/plugins/${LEGACY_PLUGIN_ID}/assets/`;
  const currentPrefix = `/plugins/${PLUGIN_ID}/assets/`;
  const rows = db
    .prepare(
      "SELECT key, value FROM plugin_settings WHERE plugin_id = ? AND key = 'currency_icon_path'"
    )
    .all(PLUGIN_ID);
  for (const row of rows) {
    if (row.value && row.value.startsWith(legacyPrefix)) {
      setPluginSetting(db, row.key, row.value.replace(legacyPrefix, currentPrefix));
    }
  }
}

function cleanupFallbackUserArtifacts(db) {
  if (!tableExists(db, "user_identities") || !tableExists(db, "user_profiles")) {
    return;
  }
  const fallbackProviders = [
    "economy_name",
    "discord_name",
    "twitch_name",
    "youtube_name"
  ];
  const rows = db
    .prepare(
      `SELECT user_id, provider, provider_user_id, display_name FROM user_identities ` +
        `WHERE provider IN (${fallbackProviders.map(() => "?").join(", ")})`
    )
    .all(...fallbackProviders);
  for (const row of rows) {
    const name = (row.display_name || row.provider_user_id || "").trim();
    if (!name) {
      continue;
    }
    const target = findUserByKnownName(db, name, providerPlatform(row.provider), row.user_id);
    if (!target || target.id === row.user_id) {
      continue;
    }
    mergeArtifactUser(db, row.user_id, target.id);
  }
}

function providerPlatform(provider) {
  if (String(provider || "").startsWith("discord")) {
    return "discord";
  }
  if (String(provider || "").startsWith("twitch")) {
    return "twitch";
  }
  if (String(provider || "").startsWith("youtube")) {
    return "youtube";
  }
  return "";
}

function mergeArtifactUser(db, fromUserId, toUserId) {
  if (!fromUserId || !toUserId || fromUserId === toUserId) {
    return;
  }
  db.transaction(() => {
    mergeStatsRows(db, fromUserId, toUserId);
    mergeEconomyRows(db, fromUserId, toUserId);
    mergeLinkedAccounts(db, fromUserId, toUserId);
    mergeUserIdentities(db, fromUserId, toUserId);
    db.prepare("DELETE FROM user_profiles WHERE id = ?").run(fromUserId);
  })();
}

function mergeStatsRows(db, fromUserId, toUserId) {
  if (!tableExists(db, "stats")) {
    return;
  }
  const from = db.prepare("SELECT messages, commands FROM stats WHERE user_id = ?").get(fromUserId);
  if (from) {
    db.prepare(
      "INSERT INTO stats (user_id, messages, commands, updated_at) VALUES (?, ?, ?, ?) " +
        "ON CONFLICT(user_id) DO UPDATE SET messages = messages + excluded.messages, commands = commands + excluded.commands, updated_at = excluded.updated_at"
    ).run(toUserId, from.messages || 0, from.commands || 0, Date.now());
    db.prepare("DELETE FROM stats WHERE user_id = ?").run(fromUserId);
  }
}

function mergeEconomyRows(db, fromUserId, toUserId) {
  if (tableExists(db, "economy_accounts")) {
    const from = db.prepare("SELECT balance FROM economy_accounts WHERE user_id = ?").get(fromUserId);
    if (from) {
      db.prepare(
        "INSERT INTO economy_accounts (user_id, balance, updated_at) VALUES (?, ?, ?) " +
          "ON CONFLICT(user_id) DO UPDATE SET balance = balance + excluded.balance, updated_at = excluded.updated_at"
      ).run(toUserId, from.balance || 0, Date.now());
      db.prepare("DELETE FROM economy_accounts WHERE user_id = ?").run(fromUserId);
    }
  }
  if (tableExists(db, "economy_transactions")) {
    db.prepare("UPDATE economy_transactions SET from_user_id = ? WHERE from_user_id = ?").run(toUserId, fromUserId);
    db.prepare("UPDATE economy_transactions SET to_user_id = ? WHERE to_user_id = ?").run(toUserId, fromUserId);
  }
  if (tableExists(db, "economy_pot_contributions")) {
    db.prepare("UPDATE economy_pot_contributions SET user_id = ? WHERE user_id = ?").run(toUserId, fromUserId);
  }
  if (tableExists(db, "economy_activity_reward_hourly")) {
    const rewards = db
      .prepare(
        "SELECT hour_start, source, amount, hits, minutes FROM economy_activity_reward_hourly WHERE user_id = ?"
      )
      .all(fromUserId);
    for (const reward of rewards) {
      db.prepare(
        "INSERT INTO economy_activity_reward_hourly (user_id, hour_start, source, amount, hits, minutes) VALUES (?, ?, ?, ?, ?, ?) " +
          "ON CONFLICT(user_id, hour_start, source) DO UPDATE SET amount = amount + excluded.amount, hits = hits + excluded.hits, minutes = minutes + excluded.minutes"
      ).run(
        toUserId,
        reward.hour_start,
        reward.source,
        reward.amount || 0,
        reward.hits || 0,
        reward.minutes || 0
      );
    }
    db.prepare("DELETE FROM economy_activity_reward_hourly WHERE user_id = ?").run(fromUserId);
  }
}

function mergeLinkedAccounts(db, fromUserId, toUserId) {
  if (!tableExists(db, "linked_accounts")) {
    return;
  }
  const accounts = db
    .prepare("SELECT id, provider FROM linked_accounts WHERE user_id = ?")
    .all(fromUserId);
  for (const account of accounts) {
    const existing = db
      .prepare("SELECT id FROM linked_accounts WHERE user_id = ? AND provider = ?")
      .get(toUserId, account.provider);
    if (existing) {
      db.prepare("DELETE FROM linked_accounts WHERE id = ?").run(account.id);
    } else {
      db.prepare("UPDATE linked_accounts SET user_id = ? WHERE id = ?").run(toUserId, account.id);
    }
  }
}

function mergeUserIdentities(db, fromUserId, toUserId) {
  const identities = db
    .prepare("SELECT id, provider, provider_user_id FROM user_identities WHERE user_id = ?")
    .all(fromUserId);
  for (const identity of identities) {
    if (String(identity.provider || "").endsWith("_name")) {
      db.prepare("DELETE FROM user_identities WHERE id = ?").run(identity.id);
      continue;
    }
    const existing = db
      .prepare("SELECT id FROM user_identities WHERE provider = ? AND provider_user_id = ? AND user_id = ?")
      .get(identity.provider, identity.provider_user_id, toUserId);
    if (existing) {
      db.prepare("DELETE FROM user_identities WHERE id = ?").run(identity.id);
    } else {
      db.prepare("UPDATE user_identities SET user_id = ?, updated_at = ? WHERE id = ?").run(
        toUserId,
        Date.now(),
        identity.id
      );
    }
  }
}

function migrateLegacyUploadDir(repoRoot) {
  const legacyDir = path.join(repoRoot, "data", LEGACY_PLUGIN_ID);
  const currentDir = path.join(repoRoot, "data", PLUGIN_ID);
  if (!fs.existsSync(legacyDir)) {
    return;
  }
  if (!fs.existsSync(currentDir)) {
    fs.renameSync(legacyDir, currentDir);
    return;
  }
  mergeDirectory(legacyDir, currentDir);
}

function mergeDirectory(sourceDir, targetDir) {
  fs.mkdirSync(targetDir, { recursive: true });
  for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
    const sourcePath = path.join(sourceDir, entry.name);
    const targetPath = path.join(targetDir, entry.name);
    if (!fs.existsSync(targetPath)) {
      fs.renameSync(sourcePath, targetPath);
      continue;
    }
    if (entry.isDirectory()) {
      mergeDirectory(sourcePath, targetPath);
    }
  }
  try {
    fs.rmSync(sourceDir, { recursive: true, force: true });
  } catch {
    // Leaving the old directory is safer than failing startup.
  }
}

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 parseJson(value, fallback) {
  if (value === undefined || value === null || value === "") {
    return fallback;
  }
  try {
    return JSON.parse(value);
  } catch {
    return fallback;
  }
}

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

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

function buildPlural(name) {
  if (!name) {
    return "";
  }
  if (name.endsWith("s")) {
    return name;
  }
  return `${name}s`;
}

function getConfig(db) {
  const now = Date.now();
  if (cachedConfig && now - cachedConfigAt < 2000) {
    return cachedConfig;
  }
  const settings = getPluginSettings(db);
  const currencyName = settings.currency_name || DEFAULT_SETTINGS.currency_name;
  const currencyPlural =
    settings.currency_name_plural ||
    buildPlural(currencyName) ||
    DEFAULT_SETTINGS.currency_name_plural;
  const bankingLabel =
    settings.banking_label || DEFAULT_SETTINGS.banking_label || "Banking";
  const bankingEnabled = parseBoolean(settings.banking_enabled, true);
  const fundName =
    settings.community_fund_name || DEFAULT_SETTINGS.community_fund_name;
  const fundPlural =
    settings.community_fund_name_plural ||
    buildPlural(fundName) ||
    DEFAULT_SETTINGS.community_fund_name_plural;
  const root = normalizeCommandRoot(settings.command_root || currencyPlural);
  const aliases = parseList(settings.command_aliases);
  const responseTemplates = buildResponseTemplates(
    parseJson(settings.response_templates, null)
  );
  const config = {
    currency: {
      name: currencyName,
      plural: currencyPlural,
      icon: settings.currency_icon_path || ""
    },
    banking: {
      label: bankingLabel,
      enabled: bankingEnabled
    },
    communityFunds: {
      name: fundName || "Community fund",
      plural: fundPlural || "Community funds"
    },
    command: {
      root: root || normalizeCommandRoot(currencyPlural) || "coins",
      aliases
    },
    platforms: {
      discord: parseBoolean(settings.platform_discord, true),
      twitch: parseBoolean(settings.platform_twitch, true),
      youtube: parseBoolean(settings.platform_youtube, true)
    },
    cooldownSeconds: parseNumber(settings.transfer_cooldown_seconds, 10),
    earn: {
      discordMessage: {
        enabled: parseBoolean(settings.earn_discord_message_enabled, true),
        amount: parseNumber(settings.earn_discord_message_amount, 1),
        cooldown: parseNumber(settings.earn_discord_message_cooldown, 30)
      },
      twitchMessage: {
        enabled: parseBoolean(settings.earn_twitch_message_enabled, true),
        amount: parseNumber(settings.earn_twitch_message_amount, 1),
        cooldown: parseNumber(settings.earn_twitch_message_cooldown, 30)
      },
      discordVoice: {
        enabled: parseBoolean(settings.earn_discord_voice_enabled, false),
        amountPerMin: parseNumber(settings.earn_discord_voice_amount_per_min, 2),
        tickMinutes: parseNumber(settings.earn_discord_voice_tick_minutes, 1)
      }
    },
    tiers: {
      discordBooster: parseNumber(settings.tier_discord_booster_multiplier, 1.25),
      twitchSub: parseNumber(settings.tier_twitch_sub_multiplier, 1.5),
      twitchMod: parseNumber(settings.tier_twitch_mod_multiplier, 1.2),
      twitchVip: parseNumber(settings.tier_twitch_vip_multiplier, 1.1),
      twitchBroadcaster: parseNumber(settings.tier_twitch_broadcaster_multiplier, 2.0)
    },
    responses: responseTemplates,
    eventsRaw: settings.custom_events || "[]"
  };
  cachedConfig = config;
  cachedConfigAt = now;
  return config;
}

function getCustomEvents(config) {
  try {
    const events = JSON.parse(config.eventsRaw || "[]");
    if (Array.isArray(events)) {
      return events
        .map((event) => ({
          id: event.id,
          name: event.name,
          amount: Number(event.amount || 0)
        }))
        .filter((event) => event.id && event.name);
    }
  } catch {
    // ignore invalid custom event config
  }
  return [];
}

function normalizeReplies(list, fallback) {
  const source = Array.isArray(list) ? list : [];
  const cleaned = source
    .map((entry) => ({
      text: (entry?.text || "").toString().trim(),
      weight: Number(entry?.weight || 1)
    }))
    .filter((entry) => entry.text);
  if (cleaned.length) {
    return cleaned;
  }
  const fallbackList = Array.isArray(fallback) ? fallback : [];
  return fallbackList.map((entry) => ({
    text: (entry?.text || "").toString(),
    weight: Number(entry?.weight || 1)
  }));
}

function buildResponseTemplates(raw) {
  const parsed = raw && typeof raw === "object" ? raw : {};
  const templates = {};
  for (const [key, base] of Object.entries(DEFAULT_RESPONSES)) {
    const override = parsed[key] || {};
    templates[key] = {
      key,
      label: base.label || key,
      mode: override.mode === "weighted" ? "weighted" : base.mode || "random",
      replies: normalizeReplies(override.replies, base.replies)
    };
  }
  for (const [key, entry] of Object.entries(parsed)) {
    if (templates[key]) {
      continue;
    }
    templates[key] = {
      key,
      label: entry?.label || key,
      mode: entry?.mode === "weighted" ? "weighted" : "random",
      replies: normalizeReplies(entry?.replies, [])
    };
  }
  return templates;
}

function getResponseTemplates(db) {
  const settings = getPluginSettings(db);
  return buildResponseTemplates(parseJson(settings.response_templates, {}));
}

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

function getHourStart(timestamp = Date.now()) {
  const hourMs = 60 * 60 * 1000;
  return Math.floor(timestamp / hourMs) * hourMs;
}

function queueActivityReward(
  db,
  { userId, source, amount, hits = 1, minutes = 0, occurredAt = Date.now() }
) {
  const numericAmount = Number(amount || 0);
  if (!userId || !source || !Number.isFinite(numericAmount) || numericAmount <= 0) {
    return;
  }
  const hourStart = getHourStart(occurredAt);
  const numericHits = Number.isFinite(Number(hits)) ? Number(hits) : 0;
  const numericMinutes = Number.isFinite(Number(minutes)) ? Number(minutes) : 0;
  db.prepare(
    "INSERT INTO economy_activity_reward_hourly (user_id, hour_start, source, amount, hits, minutes) " +
      "VALUES (?, ?, ?, ?, ?, ?) " +
      "ON CONFLICT(user_id, hour_start, source) DO UPDATE SET " +
      "amount = amount + excluded.amount, " +
      "hits = hits + excluded.hits, " +
      "minutes = minutes + excluded.minutes"
  ).run(
    userId,
    hourStart,
    source,
    Math.floor(numericAmount),
    Math.max(0, Math.floor(numericHits)),
    Math.max(0, Math.floor(numericMinutes))
  );
}

function startActivityRewardFlusher(db) {
  flushActivityRewards(db);
  if (activityFlushTimer) {
    return;
  }
  activityFlushTimer = setInterval(() => {
    try {
      flushActivityRewards(db);
    } catch (error) {
      console.error("Activity reward flush failed", error);
    }
  }, 60 * 1000);
}

function flushActivityRewards(db) {
  const currentHourStart = getHourStart();
  const rows = db
    .prepare(
      "SELECT user_id, hour_start, source, amount, hits, minutes " +
        "FROM economy_activity_reward_hourly " +
        "WHERE hour_start < ? " +
        "ORDER BY hour_start ASC"
    )
    .all(currentHourStart);
  if (!rows.length) {
    return;
  }

  const groups = new Map();
  rows.forEach((row) => {
    const key = `${row.user_id}:${row.hour_start}`;
    if (!groups.has(key)) {
      groups.set(key, {
        userId: row.user_id,
        hourStart: row.hour_start,
        rows: []
      });
    }
    groups.get(key).rows.push(row);
  });

  for (const group of groups.values()) {
    const rewards = group.rows.map((entry) => ({
      source: entry.source,
      amount: Number(entry.amount || 0),
      hits: Number(entry.hits || 0),
      minutes: Number(entry.minutes || 0),
      label: ACTIVITY_REWARD_SOURCES[entry.source] || entry.source
    }));
    const totalAmount = rewards.reduce(
      (sum, entry) => sum + Math.max(0, Number(entry.amount || 0)),
      0
    );
    if (totalAmount <= 0) {
      db.prepare(
        "DELETE FROM economy_activity_reward_hourly WHERE user_id = ? AND hour_start = ?"
      ).run(group.userId, group.hourStart);
      continue;
    }
    try {
      grantBalance(db, {
        userId: group.userId,
        amount: totalAmount,
        note: ACTIVITY_REWARD_NOTE,
        meta: {
          source: "activity_reward",
          hourStart: group.hourStart,
          hourEnd: group.hourStart + 60 * 60 * 1000,
          rewards
        }
      });
      db.prepare(
        "DELETE FROM economy_activity_reward_hourly WHERE user_id = ? AND hour_start = ?"
      ).run(group.userId, group.hourStart);
    } catch (error) {
      console.error("Failed to apply queued activity reward", error);
    }
  }
}

function registerFramework(api) {
  if (!global.lumiFrameworks) {
    global.lumiFrameworks = {};
  }
  global.lumiFrameworks.economy = api;
  global.lumiFrameworks[LEGACY_FRAMEWORK_KEY] = api;
}

function buildApi({ db }) {
  return {
    getConfig: () => getConfig(db),
    getBalance: (userId) => getBalance(db, userId),
    addBalance: ({ userId, amount, note, meta, allowFrozen }) =>
      grantBalance(db, { userId, amount, note, meta, allowFrozen }),
    removeBalance: ({ userId, amount, note, meta, allowFrozen }) =>
      spendBalance(db, { userId, amount, note, meta, allowFrozen }),
    transferBalance: ({ fromUserId, toUserId, amount, note, meta, allowFrozen }) =>
      transferBalance(db, { fromUserId, toUserId, amount, note, meta, allowFrozen }),
    createTransaction: (payload) => applyTransaction(db, payload),
    on: (event, handler) => emitter.on(event, handler),
    off: (event, handler) => emitter.off(event, handler)
  };
}

function registerCommands({ db, settings, commandRouter }) {
  if (!commandRouter) {
    return null;
  }
  const rebuild = () => {
    const config = getConfig(db);
    const platforms = [];
    if (config.platforms.discord) {
      platforms.push("discord");
    }
    if (config.platforms.twitch) {
      platforms.push("twitch");
    }
    if (config.platforms.youtube) {
      platforms.push("youtube");
    }
    if (!platforms.length) {
      commandRouter.registerCommands(PLUGIN_ID, []);
      return;
    }
    const triggers = [config.command.root, ...config.command.aliases];
    commandRouter.registerCommands(PLUGIN_ID, [
      {
        id: "economy:root",
        triggers,
        platforms,
        handler: (ctx) => handleCoinsCommand({ ctx, db, settings })
      }
    ]);
  };
  rebuild();
  return rebuild;
}

async function handleCoinsCommand({ ctx, db, settings }) {
  const config = getConfig(db);
  const prefix = settings.getSetting("command_prefix", "!");
  const root = config.command.root;
  const subcommand = (ctx.args[0] || "balance").toLowerCase();
  const args = ctx.args.slice(1);
  const usageRoot = `${prefix}${root}`;
  const baseTokens = {
    currency_name: config.currency.name,
    currency_plural: config.currency.plural,
    funds_label: config.communityFunds.plural,
    fund_label: config.communityFunds.name
  };

  if (subcommand === "help") {
    await respond(ctx, config, "help", {
      ...baseTokens,
      help: buildHelpText({ prefix, root })
    });
    return true;
  }

  if (["balance", "bal", "me"].includes(subcommand)) {
    const balance = getBalance(db, ctx.user.id);
    await respond(ctx, config, "balance_self", {
      ...baseTokens,
      balance,
      balance_text: formatCurrency(balance, config)
    });
    return true;
  }

  if (["top", "leaderboard"].includes(subcommand)) {
    const top = listTopBalances(db, 5);
    if (!top.length) {
      await respond(ctx, config, "top_empty", baseTokens);
      return true;
    }
    const lines = top
      .map((entry, index) => `${index + 1}. ${entry.username}: ${entry.balance}`)
      .join(" | ");
    await respond(ctx, config, "top_list", {
      ...baseTokens,
      lines
    });
    return true;
  }

  if (subcommand === "stats") {
    const stats = buildGlobalStats(db);
    await respond(ctx, config, "stats", {
      ...baseTokens,
      total_balance: stats.totalBalance,
      total_balance_text: formatCurrency(stats.totalBalance, config),
      total_spent: stats.totalSpent,
      total_spent_text: formatCurrency(stats.totalSpent, config)
    });
    return true;
  }

  if (["pay", "give", "transfer"].includes(subcommand)) {
    const targetToken = args[0];
    const amount = parseAmount(args[1]);
    if (!targetToken || !Number.isFinite(amount)) {
      await respond(ctx, config, "pay_missing", {
        ...baseTokens,
        usage: `${usageRoot} pay <user> <amount> [note]`
      });
      return true;
    }
    const cooldownLeft = getCooldownLeft(ctx.user.id, config);
    if (cooldownLeft > 0) {
      await respond(ctx, config, "pay_cooldown", {
        ...baseTokens,
        cooldown: cooldownLeft
      });
      return true;
    }
    const note = args.slice(2).join(" ").trim();
    const target = await resolveTargetUser(db, ctx, targetToken);
    if (!target) {
      await respond(ctx, config, "pay_not_found", baseTokens);
      return true;
    }
    if (target.profile.id === ctx.user.id) {
      await respond(ctx, config, "pay_self", baseTokens);
      return true;
    }
    const success = transferBalance(db, {
      fromUserId: ctx.user.id,
      toUserId: target.profile.id,
      amount,
      note,
      meta: { platform: ctx.platform }
    });
    if (!success.ok) {
      await respond(ctx, config, "pay_insufficient", {
        ...baseTokens,
        reason: success.message || "Transfer failed."
      });
      return true;
    }
    setCooldown(ctx.user.id);
    await respond(ctx, config, "pay_success", {
      ...baseTokens,
      amount,
      amount_text: formatCurrency(amount, config),
      target: target.label
    });
    return true;
  }

  if (["grant", "giveadmin"].includes(subcommand)) {
    const role = getRoleFlags(ctx);
    if (!role.isAdmin && !role.isMod) {
      await respond(ctx, config, "permission_denied", baseTokens);
      return true;
    }
    const targetToken = args[0];
    const amount = parseAmount(args[1]);
    if (!targetToken || !Number.isFinite(amount)) {
      await respond(ctx, config, "pay_missing", {
        ...baseTokens,
        usage: `${usageRoot} grant <user> <amount> [note]`
      });
      return true;
    }
    const note = args.slice(2).join(" ").trim();
    const target = await resolveTargetUser(db, ctx, targetToken);
    if (!target) {
      await respond(ctx, config, "pay_not_found", baseTokens);
      return true;
    }
    grantBalance(db, {
      userId: target.profile.id,
      amount,
      note,
      meta: { actorId: ctx.user.id, platform: ctx.platform }
    });
    await respond(ctx, config, "grant_success", {
      ...baseTokens,
      amount,
      amount_text: formatCurrency(amount, config),
      target: target.label
    });
    return true;
  }

  if (["take", "remove"].includes(subcommand)) {
    const role = getRoleFlags(ctx);
    if (!role.isAdmin && !role.isMod) {
      await respond(ctx, config, "permission_denied", baseTokens);
      return true;
    }
    const targetToken = args[0];
    const amount = parseAmount(args[1]);
    if (!targetToken || !Number.isFinite(amount)) {
      await respond(ctx, config, "pay_missing", {
        ...baseTokens,
        usage: `${usageRoot} take <user> <amount> [note]`
      });
      return true;
    }
    const note = args.slice(2).join(" ").trim();
    const target = await resolveTargetUser(db, ctx, targetToken);
    if (!target) {
      await respond(ctx, config, "pay_not_found", baseTokens);
      return true;
    }
    spendBalance(db, {
      userId: target.profile.id,
      amount,
      note,
      meta: { actorId: ctx.user.id, platform: ctx.platform }
    });
    await respond(ctx, config, "take_success", {
      ...baseTokens,
      amount,
      amount_text: formatCurrency(amount, config),
      target: target.label
    });
    return true;
  }

  if (["funds", "fund", "goals"].includes(subcommand)) {
    const funds = listFunds(db);
    if (!funds.length) {
      await respond(ctx, config, "funds_empty", baseTokens);
      return true;
    }
    const lines = funds
      .map((fund) => `${fund.name}: ${fund.current_amount}/${fund.target_amount}`)
      .join(" | ");
    await respond(ctx, config, "funds_list", {
      ...baseTokens,
      lines
    });
    return true;
  }

  if (subcommand === "donate") {
    const fundName = args[0];
    const amount = parseAmount(args[1]);
    if (!fundName || !Number.isFinite(amount)) {
      await respond(ctx, config, "fund_missing", {
        ...baseTokens,
        usage: `${usageRoot} donate <fund> <amount>`
      });
      return true;
    }
    const cooldownLeft = getCooldownLeft(ctx.user.id, config);
    if (cooldownLeft > 0) {
      await respond(ctx, config, "pay_cooldown", {
        ...baseTokens,
        cooldown: cooldownLeft
      });
      return true;
    }
    const fund = findFund(db, fundName);
    if (!fund || fund.status !== "active") {
      await respond(ctx, config, "fund_not_found", baseTokens);
      return true;
    }
    const success = spendBalance(db, {
      userId: ctx.user.id,
      amount,
      note: `Donation to ${fund.name}`,
      meta: { fundId: fund.id }
    });
    if (!success.ok) {
      await respond(ctx, config, "pay_insufficient", {
        ...baseTokens,
        reason: success.message || "Donation failed."
      });
      return true;
    }
    addFundContribution(db, fund.id, ctx.user.id, amount);
    setCooldown(ctx.user.id);
    await respond(ctx, config, "fund_donate_success", {
      ...baseTokens,
      amount,
      amount_text: formatCurrency(amount, config),
      fund: fund.name
    });
    return true;
  }

  if (subcommand === "reward") {
    const role = getRoleFlags(ctx);
    if (!role.isAdmin && !role.isMod) {
      await respond(ctx, config, "permission_denied", baseTokens);
      return true;
    }
    const eventKey = args[0];
    const targetToken = args[1];
    if (!eventKey || !targetToken) {
      await respond(ctx, config, "reward_missing", {
        ...baseTokens,
        usage: `${usageRoot} reward <event> <user>`
      });
      return true;
    }
    const event = getCustomEvents(config).find(
      (entry) => entry.id === eventKey || entry.name.toLowerCase() === eventKey.toLowerCase()
    );
    if (!event) {
      await respond(ctx, config, "reward_not_found", baseTokens);
      return true;
    }
    const target = await resolveTargetUser(db, ctx, targetToken);
    if (!target) {
      await respond(ctx, config, "pay_not_found", baseTokens);
      return true;
    }
    grantBalance(db, {
      userId: target.profile.id,
      amount: event.amount,
      note: `Event reward: ${event.name}`,
      meta: { eventId: event.id }
    });
    await respond(ctx, config, "reward_success", {
      ...baseTokens,
      amount: event.amount,
      amount_text: formatCurrency(event.amount, config),
      target: target.label
    });
    return true;
  }

  await respond(ctx, config, "help", {
    ...baseTokens,
    help: buildHelpText({ prefix, root })
  });
  return true;
}

function buildHelpText({ prefix, root }) {
  return (
    `Commands: ${prefix}${root} balance | ${prefix}${root} pay <user> <amount> | ` +
    `${prefix}${root} top | ${prefix}${root} stats | ${prefix}${root} funds | ` +
    `${prefix}${root} donate <fund> <amount>`
  );
}

function getRoleFlags(ctx) {
  if (ctx.platform === "discord") {
    const roles = ctx.meta?.message?.member?.roles?.cache;
    if (!roles) {
      return { isAdmin: false, isMod: false };
    }
    const adminIds = parseList(settingsApi?.getSetting?.("discord_admin_role_id"));
    const modIds = parseList(settingsApi?.getSetting?.("discord_mod_role_id"));
    const roleIds = Array.from(roles.keys());
    const isAdmin = roleIds.some((roleId) => adminIds.includes(roleId));
    const isMod = roleIds.some((roleId) => modIds.includes(roleId));
    return { isAdmin, isMod };
  }
  if (ctx.platform === "twitch") {
    const badges = ctx.meta?.tags?.badges || {};
    const isAdmin = Boolean(badges.broadcaster);
    const isMod = Boolean(ctx.meta?.tags?.mod || badges.moderator);
    return { isAdmin, isMod };
  }
  if (ctx.platform === "youtube") {
    const author = ctx.meta?.author || {};
    const isAdmin = Boolean(author.isChatOwner);
    const isMod = Boolean(author.isChatModerator);
    return { isAdmin, isMod };
  }
  return { isAdmin: false, isMod: false };
}

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

function parseSignedAmount(value) {
  if (value === undefined || value === null) {
    return NaN;
  }
  const number = Number(value);
  if (!Number.isFinite(number)) {
    return NaN;
  }
  if (number === 0) {
    return 0;
  }
  const rounded = number > 0 ? Math.floor(number) : Math.ceil(number);
  return rounded;
}

function formatCurrency(amount, config) {
  const name = amount === 1 ? config.currency.name : config.currency.plural;
  return `${amount} ${name}`;
}

function pickResponse(template) {
  const replies = Array.isArray(template?.replies) ? template.replies : [];
  if (!replies.length) {
    return "";
  }
  if (template.mode === "weighted") {
    const total = replies.reduce(
      (sum, entry) => sum + Math.max(0, Number(entry.weight || 0)),
      0
    );
    if (total > 0) {
      let roll = Math.random() * total;
      for (const entry of replies) {
        roll -= Math.max(0, Number(entry.weight || 0));
        if (roll <= 0) {
          return entry.text || "";
        }
      }
    }
  }
  const fallback = replies[Math.floor(Math.random() * replies.length)];
  return fallback?.text || "";
}

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

function buildResponse(config, key, tokens) {
  const template = config.responses?.[key] || DEFAULT_RESPONSES[key];
  if (!template) {
    return "";
  }
  const text = pickResponse(template);
  if (!text) {
    return "";
  }
  return renderTemplate(text, tokens);
}

async function respond(ctx, config, key, tokens) {
  const message = buildResponse(config, key, tokens);
  if (!message) {
    return;
  }
  await ctx.reply(message);
}

function escapeHtml(value) {
  return (value || "")
    .toString()
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

function installProfileHook(app, getConfig) {
  if (!app || app.__economyProfileHookInstalled) {
    return;
  }
  app.__economyProfileHookInstalled = true;
  const originalRender = app.render.bind(app);
  app.render = (view, options, callback) => {
    if (typeof options === "function") {
      callback = options;
      options = {};
    }
    if (typeof callback !== "function" || view !== "profile") {
      return originalRender(view, options, callback);
    }
    const config = getConfig ? getConfig() : null;
    if (!config?.banking?.enabled) {
      return originalRender(view, options, callback);
    }
    return originalRender(view, options, (err, html) => {
      if (err) {
        return callback(err);
      }
      try {
        if (!html.includes('href="/profile/banking"')) {
          const label = escapeHtml(config.banking.label || "Banking");
          const marker = '<div class="profile-actions">';
          if (html.includes(marker)) {
            html = html.replace(
              marker,
              `${marker}\n    <a class="button" href="/profile/banking">${label}</a>`
            );
          }
        }
      } catch {
        // ignore injection errors
      }
      return callback(null, html);
    });
  };
}

function getCooldownLeft(userId, config) {
  const last = transferCooldowns.get(userId) || 0;
  const now = Date.now();
  const cooldown = (config.cooldownSeconds || 10) * 1000;
  const diff = cooldown - (now - last);
  return diff > 0 ? Math.ceil(diff / 1000) : 0;
}

function setCooldown(userId) {
  transferCooldowns.set(userId, Date.now());
}

async function resolveTargetUser(db, ctx, token) {
  if (!token) {
    return null;
  }
  if (ctx.platform === "discord") {
    const message = ctx.meta?.message;
    const mentionId = token.match(/^<@!?(\d+)>$/)?.[1] || null;
    const mention = mentionId ? message?.mentions?.users?.get?.(mentionId) : null;
    if (mention?.id) {
      const display =
        mention.globalName || mention.username || mention.tag || mention.id;
      const profile = ensureUserForIdentity({
        provider: "discord",
        providerUserId: mention.id,
        displayName: display,
        avatar: mention.avatar
          ? `https://cdn.discordapp.com/avatars/${mention.id}/${mention.avatar}.png?size=128`
          : null
      });
      return { profile, label: `<@${mention.id}>` };
    }
    const idMatch = token.match(/^(\d{15,})$/) || (mentionId ? [token, mentionId] : null);
    if (idMatch) {
      const profile = ensureUserForIdentity({
        provider: "discord",
        providerUserId: idMatch[1],
        displayName: idMatch[1]
      });
      return { profile, label: `<@${idMatch[1]}>` };
    }
  }

  const cleaned = token.replace(/^@/, "").trim();
  if (!cleaned) {
    return null;
  }

  const internal = findUserByInternalName(db, cleaned);
  if (internal) {
    return { profile: internal, label: internal.internal_username };
  }

  const knownUser = findUserByKnownName(db, cleaned, ctx.platform);
  if (knownUser) {
    return { profile: knownUser, label: knownUser.internal_username };
  }

  if (ctx.platform === "twitch") {
    const profile = ensureUserForIdentity({
      provider: "twitch_login",
      providerUserId: cleaned.toLowerCase(),
      displayName: cleaned,
      fallbackName: cleaned
    });
    return { profile, label: `@${cleaned}` };
  }

  if (ctx.platform === "youtube") {
    const profile = ensureUserForIdentity({
      provider: "youtube_name",
      providerUserId: cleaned.toLowerCase(),
      displayName: cleaned,
      fallbackName: cleaned
    });
    return { profile, label: cleaned };
  }

  const profile = ensureUserForIdentity({
    provider: "economy_name",
    providerUserId: cleaned.toLowerCase(),
    displayName: cleaned,
    fallbackName: cleaned
  });
  return { profile, label: cleaned };
}

function findUserByInternalName(db, name) {
  return db
    .prepare(
      "SELECT id, internal_username FROM user_profiles WHERE lower(internal_username) = lower(?)"
    )
    .get(name);
}

function findUserByKnownName(db, name, platform, excludeUserId = null) {
  const cleaned = (name || "").trim();
  if (!cleaned) {
    return null;
  }
  const providerHints = getPlatformProviderHints(platform);
  const providerScore = providerHints.length
    ? `CASE WHEN provider IN (${providerHints.map(() => "?").join(", ")}) THEN 0 ELSE 1 END,`
    : "";
  const providerParams = providerHints.length ? providerHints : [];
  const identity = db
    .prepare(
      "SELECT user_profiles.id AS id, user_profiles.internal_username AS internal_username " +
        "FROM user_identities " +
        "JOIN user_profiles ON user_profiles.id = user_identities.user_id " +
        "WHERE (lower(user_identities.display_name) = lower(?) " +
        "OR lower(user_identities.provider_user_id) = lower(?)) " +
        "AND user_identities.user_id != ? " +
        "AND user_identities.provider NOT LIKE '%_name' " +
        `ORDER BY ${providerScore} user_identities.updated_at DESC LIMIT 1`
    )
    .get(cleaned, cleaned, excludeUserId || "", ...providerParams);
  if (identity) {
    return identity;
  }
  if (!tableExists(db, "linked_accounts")) {
    return null;
  }
  const linked = db
    .prepare(
      "SELECT user_profiles.id AS id, user_profiles.internal_username AS internal_username " +
        "FROM linked_accounts " +
        "JOIN user_profiles ON user_profiles.id = linked_accounts.user_id " +
        "WHERE (lower(linked_accounts.display_name) = lower(?) " +
        "OR lower(linked_accounts.provider_user_id) = lower(?)) " +
        "AND linked_accounts.user_id != ? " +
        `ORDER BY ${providerScore.replaceAll("provider", "linked_accounts.provider")} linked_accounts.updated_at DESC LIMIT 1`
    )
    .get(cleaned, cleaned, excludeUserId || "", ...providerParams);
  return linked || null;
}

function getPlatformProviderHints(platform) {
  if (platform === "discord") {
    return ["discord"];
  }
  if (platform === "twitch") {
    return ["twitch", "twitch_login"];
  }
  if (platform === "youtube") {
    return ["youtube", "youtube_name"];
  }
  return [];
}

function ensureAccount(db, userId) {
  db.prepare(
    "INSERT INTO economy_accounts (user_id, balance, updated_at) VALUES (?, 0, ?) " +
      "ON CONFLICT(user_id) DO UPDATE SET updated_at = excluded.updated_at"
  ).run(userId, Date.now());
}

function getBalance(db, userId) {
  if (!userId) {
    return 0;
  }
  const row = db
    .prepare("SELECT balance FROM economy_accounts WHERE user_id = ?")
    .get(userId);
  return row?.balance ?? 0;
}

function updateBalance(db, userId, delta) {
  ensureAccount(db, userId);
  db.prepare(
    "UPDATE economy_accounts SET balance = balance + ?, updated_at = ? WHERE user_id = ?"
  ).run(delta, Date.now(), userId);
}

function isFrozenUser(userId) {
  try {
    return Boolean(global.lumiModeration?.isFrozen?.(userId));
  } catch {
    return false;
  }
}

function applyTransaction(db, payload) {
  const amount = Math.abs(Number(payload.amount));
  if (!Number.isFinite(amount) || amount <= 0) {
    throw new Error("Invalid amount.");
  }
  const id = payload.id || crypto.randomUUID();
  const now = Date.now();
  const fromUserId = payload.fromUserId || null;
  const toUserId = payload.toUserId || null;
  const note = payload.note || null;
  const meta = payload.meta ? JSON.stringify(payload.meta) : null;
  if (!payload.allowFrozen) {
    if (fromUserId && isFrozenUser(fromUserId)) {
      throw new Error("Account is frozen.");
    }
    if (toUserId && isFrozenUser(toUserId)) {
      throw new Error("Account is frozen.");
    }
  }

  db.transaction(() => {
    if (fromUserId) {
      ensureAccount(db, fromUserId);
      const current = getBalance(db, fromUserId);
      if (!payload.allowNegative && current < amount) {
        throw new Error("Insufficient balance.");
      }
      updateBalance(db, fromUserId, -amount);
    }
    if (toUserId) {
      ensureAccount(db, toUserId);
      updateBalance(db, toUserId, amount);
    }
    db.prepare(
      "INSERT INTO economy_transactions (id, type, amount, from_user_id, to_user_id, note, meta, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
    ).run(
      id,
      payload.type || "transaction",
      amount,
      fromUserId,
      toUserId,
      note,
      meta,
      now
    );
  })();

  emitter.emit("transaction", {
    id,
    type: payload.type,
    amount,
    fromUserId,
    toUserId,
    note,
    meta: payload.meta || null,
    createdAt: now
  });
  return id;
}

function transferBalance(db, { fromUserId, toUserId, amount, note, meta, allowFrozen }) {
  try {
    applyTransaction(db, {
      type: "transfer",
      amount,
      fromUserId,
      toUserId,
      note,
      meta,
      allowNegative: false,
      allowFrozen: Boolean(allowFrozen)
    });
    return { ok: true };
  } catch (error) {
    return { ok: false, message: error.message || "Transfer failed." };
  }
}

function grantBalance(db, { userId, amount, note, meta, allowFrozen }) {
  return applyTransaction(db, {
    type: "earn",
    amount,
    fromUserId: null,
    toUserId: userId,
    note,
    meta,
    allowNegative: true,
    allowFrozen: Boolean(allowFrozen)
  });
}

function spendBalance(db, { userId, amount, note, meta, allowFrozen }) {
  try {
    applyTransaction(db, {
      type: "spend",
      amount,
      fromUserId: userId,
      toUserId: null,
      note,
      meta,
      allowNegative: false,
      allowFrozen: Boolean(allowFrozen)
    });
    return { ok: true };
  } catch (error) {
    return { ok: false, message: error.message || "Spend failed." };
  }
}

function adjustBalance(db, { userId, amount, note, meta }) {
  if (amount === 0) {
    return;
  }
  if (amount > 0) {
    applyTransaction(db, {
      type: "adjust",
      amount,
      fromUserId: null,
      toUserId: userId,
      note,
      meta,
      allowNegative: true
    });
    return;
  }
  applyTransaction(db, {
    type: "adjust",
    amount: Math.abs(amount),
    fromUserId: userId,
    toUserId: null,
    note,
    meta,
    allowNegative: true
  });
}

function listTransactions(db, { userId, limit }) {
  const params = [];
  let where = "";
  if (userId) {
    where = "WHERE t.from_user_id = ? OR t.to_user_id = ?";
    params.push(userId, userId);
  }
  params.push(limit || 100);
  return db
    .prepare(
      "SELECT t.*, fromUser.internal_username AS from_name, toUser.internal_username AS to_name " +
        "FROM economy_transactions t " +
        "LEFT JOIN user_profiles AS fromUser ON fromUser.id = t.from_user_id " +
        "LEFT JOIN user_profiles AS toUser ON toUser.id = t.to_user_id " +
        `${where} ORDER BY t.created_at DESC LIMIT ?`
    )
    .all(...params)
    .map((row) => normalizeTransactionRow(row));
}

function normalizeTransactionRow(row) {
  const tx = { ...row };
  const note = (row.note || "").toString();
  const meta = parseTransactionMeta(row.meta);
  tx.meta_object = meta;
  tx.note_display = note || "-";
  tx.note_search = note || "";
  tx.activity_reward = null;

  if (meta?.source === "activity_reward") {
    const rewards = Array.isArray(meta.rewards)
      ? meta.rewards
          .map((entry) => ({
            source: (entry?.source || "").toString(),
            label:
              ACTIVITY_REWARD_SOURCES[(entry?.source || "").toString()] ||
              (entry?.label || entry?.source || "Activity"),
            amount: Number(entry?.amount || 0),
            hits: Number(entry?.hits || 0),
            minutes: Number(entry?.minutes || 0)
          }))
          .filter((entry) => entry.amount > 0)
      : [];
    tx.activity_reward = {
      hourStart: Number(meta.hourStart || 0),
      hourEnd: Number(meta.hourEnd || 0),
      rewards
    };
    tx.note_display = ACTIVITY_REWARD_NOTE;
    tx.note_search = [
      ACTIVITY_REWARD_NOTE,
      ...rewards.map((entry) => `${entry.label} ${entry.amount} ${entry.hits} ${entry.minutes}`)
    ].join(" ");
  }
  return tx;
}

function parseTransactionMeta(rawMeta) {
  if (!rawMeta) {
    return null;
  }
  if (typeof rawMeta === "object") {
    return rawMeta;
  }
  try {
    return JSON.parse(rawMeta);
  } catch {
    return null;
  }
}

function buildGlobalStats(db) {
  const totalBalance = db
    .prepare("SELECT COALESCE(SUM(balance), 0) AS total FROM economy_accounts")
    .get();
  const totalSpent = db
    .prepare(
      "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
        "WHERE from_user_id IS NOT NULL AND (to_user_id IS NULL OR to_user_id = '')"
    )
    .get();
  const totalTransactions = db
    .prepare("SELECT COUNT(*) AS count FROM economy_transactions")
    .get();
  return {
    totalBalance: totalBalance?.total || 0,
    totalSpent: totalSpent?.total || 0,
    totalTransactions: totalTransactions?.count || 0
  };
}

function buildUserStats(db, userId) {
  if (!userId) {
    return {
      balance: 0,
      totalEarned: 0,
      totalSpent: 0,
      totalReceived: 0,
      totalSent: 0
    };
  }
  const balance = getBalance(db, userId);
  const totalEarned = db
    .prepare(
      "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
        "WHERE to_user_id = ? AND (from_user_id IS NULL OR from_user_id = '')"
    )
    .get(userId);
  const totalSpent = db
    .prepare(
      "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
        "WHERE from_user_id = ? AND (to_user_id IS NULL OR to_user_id = '')"
    )
    .get(userId);
  const totalReceived = db
    .prepare(
      "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
        "WHERE to_user_id = ? AND from_user_id IS NOT NULL AND from_user_id != ''"
    )
    .get(userId);
  const totalSent = db
    .prepare(
      "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
        "WHERE from_user_id = ? AND to_user_id IS NOT NULL AND to_user_id != ''"
    )
    .get(userId);
  return {
    balance,
    totalEarned: totalEarned?.total || 0,
    totalSpent: totalSpent?.total || 0,
    totalReceived: totalReceived?.total || 0,
    totalSent: totalSent?.total || 0
  };
}

function listTopBalances(db, limit) {
  return db
    .prepare(
      "SELECT user_profiles.internal_username AS username, economy_accounts.balance AS balance " +
        "FROM economy_accounts " +
        "JOIN user_profiles ON user_profiles.id = economy_accounts.user_id " +
        "ORDER BY economy_accounts.balance DESC LIMIT ?"
    )
    .all(limit);
}

function listFunds(db) {
  return db
    .prepare("SELECT * FROM economy_pots WHERE status != 'archived' ORDER BY name")
    .all();
}

function findFund(db, name) {
  return db
    .prepare("SELECT * FROM economy_pots WHERE lower(name) = lower(?)")
    .get(name);
}

function createFund(db, { name, description, targetAmount }) {
  const now = Date.now();
  db.prepare(
    "INSERT INTO economy_pots (id, name, description, target_amount, current_amount, status, created_at, updated_at) VALUES (?, ?, ?, ?, 0, 'active', ?, ?)"
  ).run(crypto.randomUUID(), name, description || "", targetAmount || 0, now, now);
}

function updateFund(db, { id, name, description, targetAmount, status }) {
  db.prepare(
    "UPDATE economy_pots SET name = ?, description = ?, target_amount = ?, status = ?, updated_at = ? WHERE id = ?"
  ).run(
    name,
    description || "",
    Number.isFinite(targetAmount) ? targetAmount : 0,
    status || "active",
    Date.now(),
    id
  );
}

function addFundContribution(db, fundId, userId, amount) {
  const now = Date.now();
  db.transaction(() => {
    db.prepare(
      "INSERT INTO economy_pot_contributions (id, pot_id, user_id, amount, created_at) VALUES (?, ?, ?, ?, ?)"
    ).run(crypto.randomUUID(), fundId, userId, amount, now);
    db.prepare(
      "UPDATE economy_pots SET current_amount = current_amount + ?, updated_at = ? WHERE id = ?"
    ).run(amount, now, fundId);
  })();
}

function attachDiscordListeners({ db, settings, discordClient }) {
  if (!discordClient) {
    return;
  }
  discordClient.on("messageCreate", (message) => {
    if (!message || message.author?.bot) {
      return;
    }
    const config = getConfig(db);
    if (!config.platforms.discord || !config.earn.discordMessage.enabled) {
      return;
    }
    const userId = message.author.id;
    const key = `discord:${userId}`;
    const last = messageCooldowns.get(key) || 0;
    const now = Date.now();
    if (now - last < config.earn.discordMessage.cooldown * 1000) {
      return;
    }
    const displayName =
      message.author.globalName || message.author.username || message.author.tag;
    const profile = ensureUserForIdentity({
      provider: "discord",
      providerUserId: userId,
      displayName,
      avatar: message.author.avatar
        ? `https://cdn.discordapp.com/avatars/${userId}/${message.author.avatar}.png?size=128`
        : null
    });
    const multiplier = getDiscordTierMultiplier(message, config);
    const reward = Math.max(
      0,
      Math.floor(config.earn.discordMessage.amount * multiplier)
    );
    if (reward > 0) {
      queueActivityReward(db, {
        userId: profile.id,
        source: "discord_message",
        amount: reward,
        hits: 1
      });
      messageCooldowns.set(key, now);
    }
  });

  discordClient.on("voiceStateUpdate", (_oldState, newState) => {
    if (!newState?.member || newState.member.user?.bot) {
      return;
    }
    const userId = newState.member.id;
    const joined = Boolean(newState.channelId);
    if (!joined) {
      voiceStates.delete(userId);
      return;
    }
    if (!voiceStates.has(userId)) {
      voiceStates.set(userId, {
        member: newState.member,
        lastAwardAt: Date.now()
      });
    }
  });

  if (!voiceTimer) {
    voiceTimer = setInterval(() => {
      const config = getConfig(db);
      if (!config.platforms.discord || !config.earn.discordVoice.enabled) {
        return;
      }
      const tickMs = Math.max(1, config.earn.discordVoice.tickMinutes) * 60 * 1000;
      const rewardBase = config.earn.discordVoice.amountPerMin;
      const now = Date.now();
      for (const [userId, state] of voiceStates.entries()) {
        const elapsed = now - state.lastAwardAt;
        if (elapsed < tickMs) {
          continue;
        }
        const minutes = Math.floor(elapsed / tickMs);
        const multiplier = getDiscordVoiceMultiplier(state.member, config);
        const reward = Math.max(0, Math.floor(rewardBase * minutes * multiplier));
        if (reward > 0) {
          const profile = ensureUserForIdentity({
            provider: "discord",
            providerUserId: userId,
            displayName:
              state.member.user.globalName ||
              state.member.user.username ||
              state.member.user.tag
          });
          queueActivityReward(db, {
            userId: profile.id,
            source: "discord_voice",
            amount: reward,
            hits: 1,
            minutes: minutes * Math.max(1, config.earn.discordVoice.tickMinutes)
          });
        }
        state.lastAwardAt = now;
      }
    }, 30000);
  }
}

function getDiscordTierMultiplier(message, config) {
  const boosterRoleId = message.guild?.premiumSubscriberRole?.id;
  if (!boosterRoleId) {
    return 1;
  }
  const hasBooster = message.member?.roles?.cache?.has(boosterRoleId);
  return hasBooster ? config.tiers.discordBooster : 1;
}

function getDiscordVoiceMultiplier(member, config) {
  const boosterRoleId = member?.guild?.premiumSubscriberRole?.id;
  if (!boosterRoleId) {
    return 1;
  }
  const hasBooster = member.roles?.cache?.has(boosterRoleId);
  return hasBooster ? config.tiers.discordBooster : 1;
}

function attachTwitchListeners({ db, settings, twitchClient }) {
  if (!twitchClient) {
    return;
  }
  twitchClient.on("message", (_channel, tags, _message, self) => {
    if (self) {
      return;
    }
    const config = getConfig(db);
    if (!config.platforms.twitch || !config.earn.twitchMessage.enabled) {
      return;
    }
    const userId = tags["user-id"];
    if (!userId) {
      return;
    }
    const key = `twitch:${userId}`;
    const last = messageCooldowns.get(key) || 0;
    const now = Date.now();
    if (now - last < config.earn.twitchMessage.cooldown * 1000) {
      return;
    }
    const displayName = tags["display-name"] || tags.username;
    const profile = ensureUserForIdentity({
      provider: "twitch",
      providerUserId: userId,
      displayName
    });
    const multiplier = getTwitchTierMultiplier(tags, config);
    const reward = Math.max(0, Math.floor(config.earn.twitchMessage.amount * multiplier));
    if (reward > 0) {
      queueActivityReward(db, {
        userId: profile.id,
        source: "twitch_message",
        amount: reward,
        hits: 1
      });
      messageCooldowns.set(key, now);
    }
  });
}

function getTwitchTierMultiplier(tags, config) {
  const badges = tags.badges || {};
  if (badges.broadcaster) {
    return config.tiers.twitchBroadcaster;
  }
  if (badges.moderator || tags.mod) {
    return config.tiers.twitchMod;
  }
  if (badges.vip) {
    return config.tiers.twitchVip;
  }
  if (tags.subscriber) {
    return config.tiers.twitchSub;
  }
  return 1;
}
