const path = require("path");

const PLUGIN_ID = "quotes";

let cachedAppToken = null;
let cachedAppTokenExpiry = 0;

module.exports = {
  id: PLUGIN_ID,
  init({ web, db, settings, commandRouter }) {
    ensureTables(db);
    registerQuoteCommands({ db, settings, commandRouter });

    const router = web.createRouter();

    router.get("/", (req, res) => {
      const user = req.session.user || null;
      const isMod = Boolean(user?.isAdmin || user?.isMod);
      if (!isMod) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      const quotes = listQuotes(db, { includeHidden: true, includeArchived: true });
      const editId = parseInt(req.query.edit, 10);
      const editingQuote = Number.isFinite(editId)
        ? getQuoteById(db, editId, { includeHidden: true, includeArchived: true })
        : null;
      res.render(path.join(__dirname, "views", "quotes.ejs"), {
        title: "Quotes",
        quotes,
        editingQuote,
        formatDateTime,
        formatDateInput
      });
    });

    router.post("/quotes/create", (req, res) => {
      const user = req.session.user || null;
      const isMod = Boolean(user?.isAdmin || user?.isMod);
      if (!isMod) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      const quoteText = (req.body.quote_text || "").trim();
      if (!quoteText) {
        req.session.flash = { type: "error", message: "Quote text is required." };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      const quoteDatetime = parseDateInput(req.body.quote_datetime) || Date.now();
      const quoter = (req.body.quoter || user?.username || "Unknown").trim();
      const quoterUserId = resolveUserIdByUsername(db, quoter);
      const gameName = (req.body.game_name || "").trim();
      const hidden = req.body.hidden === "on";
      const archived = req.body.archived === "on";
      const now = Date.now();
      addQuote(db, {
        quoteText,
        quoter: quoter || "Unknown",
        quoterUserId,
        gameName: gameName || null,
        quoteDatetime,
        editedBy: user?.username || "system",
        editedLast: now,
        hidden,
        archived
      });
      req.session.flash = { type: "success", message: "Quote added." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/quotes/:id/update", (req, res) => {
      const user = req.session.user || null;
      const isMod = Boolean(user?.isAdmin || user?.isMod);
      if (!isMod) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      const id = parseInt(req.params.id, 10);
      if (!Number.isFinite(id)) {
        req.session.flash = { type: "error", message: "Invalid quote id." };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      const quoteText = (req.body.quote_text || "").trim();
      if (!quoteText) {
        req.session.flash = { type: "error", message: "Quote text is required." };
        return res.redirect(`/plugins/${PLUGIN_ID}?edit=${id}`);
      }
      const quoteDatetime = parseDateInput(req.body.quote_datetime);
      const quoter = (req.body.quoter || "").trim();
      const quoterUserId = resolveUserIdByUsername(db, quoter);
      const gameName = (req.body.game_name || "").trim();
      const hidden = req.body.hidden === "on";
      const archived = req.body.archived === "on";
      updateQuote(db, id, {
        quoteText,
        quoter: quoter || "Unknown",
        quoterUserId,
        gameName: gameName || null,
        quoteDatetime,
        hidden,
        archived,
        editedBy: user?.username || "system",
        editedLast: Date.now()
      });
      req.session.flash = { type: "success", message: "Quote updated." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/quotes/:id/hide", (req, res) => {
      const user = req.session.user || null;
      const isMod = Boolean(user?.isAdmin || user?.isMod);
      if (!isMod) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      setQuoteHidden(db, req.params.id, true, user?.username || "system");
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/quotes/:id/unhide", (req, res) => {
      const user = req.session.user || null;
      const isMod = Boolean(user?.isAdmin || user?.isMod);
      if (!isMod) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      setQuoteHidden(db, req.params.id, false, user?.username || "system");
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/quotes/:id/archive", (req, res) => {
      const user = req.session.user || null;
      const isMod = Boolean(user?.isAdmin || user?.isMod);
      if (!isMod) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      setQuoteArchived(db, req.params.id, true, user?.username || "system");
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/quotes/:id/restore", (req, res) => {
      const user = req.session.user || null;
      const isMod = Boolean(user?.isAdmin || user?.isMod);
      if (!isMod) {
        return res.status(403).render("error", {
          title: "Access denied",
          message: "You do not have access to that page."
        });
      }
      setQuoteArchived(db, req.params.id, false, user?.username || "system");
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.get("/api/quotes", (req, res) => {
      const user = req.session.user || null;
      const isMod = Boolean(user?.isAdmin || user?.isMod);
      if (!isMod) {
        return res.status(403).json({ error: "Access denied." });
      }
      const quotes = listQuotes(db, { includeHidden: true, includeArchived: false });
      res.json({ quotes });
    });

    router.get("/api/quotes/:id", (req, res) => {
      const user = req.session.user || null;
      const isMod = Boolean(user?.isAdmin || user?.isMod);
      if (!isMod) {
        return res.status(403).json({ error: "Access denied." });
      }
      const id = parseInt(req.params.id, 10);
      if (!Number.isFinite(id)) {
        return res.status(400).json({ error: "Invalid quote id." });
      }
      const quote = getQuoteById(db, id, { includeHidden: true, includeArchived: false });
      if (!quote) {
        return res.status(404).json({ error: "Quote not found." });
      }
      res.json({ quote });
    });

    web.mount(`/plugins/${PLUGIN_ID}`, router, {
      label: "Quotes",
      role: "mod",
      section: "plugins"
    });
  }
};

function ensureTables(db) {
  db.exec(`
    CREATE TABLE IF NOT EXISTS quotes (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      quote_text TEXT NOT NULL,
      quoter TEXT NOT NULL,
      quoter_user_id TEXT,
      game_name TEXT,
      quote_datetime INTEGER NOT NULL,
      edited_by TEXT,
      edited_last INTEGER,
      hidden INTEGER NOT NULL DEFAULT 0,
      archived INTEGER NOT NULL DEFAULT 0
    );

    CREATE INDEX IF NOT EXISTS quotes_archived_idx ON quotes (archived);
    CREATE INDEX IF NOT EXISTS quotes_hidden_idx ON quotes (hidden);
    CREATE INDEX IF NOT EXISTS quotes_quote_datetime_idx ON quotes (quote_datetime);
  `);

  const columns = db
    .prepare("PRAGMA table_info(quotes)")
    .all()
    .map((column) => column.name);
  if (!columns.includes("quoter_user_id")) {
    db.exec("ALTER TABLE quotes ADD COLUMN quoter_user_id TEXT");
  }
  db.exec("CREATE INDEX IF NOT EXISTS quotes_quoter_user_id_idx ON quotes (quoter_user_id)");
  try {
    db.prepare(
      "UPDATE quotes SET quoter_user_id = (" +
        "SELECT id FROM user_profiles WHERE internal_username = quotes.quoter COLLATE NOCASE LIMIT 1" +
        ") WHERE quoter_user_id IS NULL"
    ).run();
  } catch {
    // ignore backfill errors
  }
}

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

  const platforms = ["discord", "twitch", "youtube"];
  commandRouter.registerCommands(PLUGIN_ID, [
    {
      id: "quote",
      triggers: ["quote"],
      platforms,
      handler: async (ctx) => await handleQuoteCommand({ ctx, db, settings })
    }
  ]);
  return null;
}

async function handleQuoteCommand({ ctx, db, settings }) {
  const prefix = settings.getSetting("command_prefix", "!");
  const subcommand = (ctx.args[0] || "").toLowerCase();
  const role = getRoleFlags(ctx, settings);

  if (!subcommand) {
    await ctx.reply(
      `Usage: ${prefix}quote <id|random|search|add|remove> | ${prefix}quote random`
    );
    return true;
  }

  if (subcommand === "add") {
    if (!role.isAdmin && !role.isMod) {
      await ctx.reply("You do not have permission to add quotes.");
      return true;
    }
    const quoteText = ctx.args.slice(1).join(" ").trim();
    if (!quoteText) {
      await ctx.reply(`Usage: ${prefix}quote add <quote text>`);
      return true;
    }
    const gameName = await resolveGameName(ctx, settings);
    const now = Date.now();
    const quoter = ctx.user.displayName || ctx.user.username || "Unknown";
    const editor = ctx.user.username || quoter;
    const id = addQuote(db, {
      quoteText,
      quoter,
      quoterUserId: ctx.user.id,
      gameName,
      quoteDatetime: now,
      editedBy: editor,
      editedLast: now,
      hidden: false,
      archived: false
    });
    await ctx.reply(`Quote #${id} added.`);
    return true;
  }

  if (subcommand === "search") {
    const searchText = ctx.args.slice(1).join(" ").trim();
    if (!searchText) {
      await ctx.reply(`Usage: ${prefix}quote search <text>`);
      return true;
    }
    const match = searchQuotes(db, searchText);
    if (!match) {
      await ctx.reply("No matching quotes found.");
      return true;
    }
    await replyWithQuote(ctx, match);
    return true;
  }

  if (subcommand === "remove" || subcommand === "delete") {
    if (!role.isAdmin && !role.isMod) {
      await ctx.reply("You do not have permission to remove quotes.");
      return true;
    }
    const id = parseInt(ctx.args[1], 10);
    if (!Number.isFinite(id)) {
      await ctx.reply(`Usage: ${prefix}quote remove <id>`);
      return true;
    }
    const removed = setQuoteArchived(db, id, true, ctx.user.username);
    if (!removed) {
      await ctx.reply("Quote not found.");
      return true;
    }
    await ctx.reply(`Quote #${id} archived.`);
    return true;
  }

  if (subcommand === "random") {
    const quote = getRandomQuote(db);
    if (!quote) {
      await ctx.reply("No quotes available yet.");
      return true;
    }
    await replyWithQuote(ctx, quote);
    return true;
  }

  if (/^\d+$/.test(subcommand)) {
    const id = parseInt(subcommand, 10);
    const quote = getQuoteById(db, id, { includeHidden: false, includeArchived: false });
    if (!quote) {
      await ctx.reply("Quote not found.");
      return true;
    }
    await replyWithQuote(ctx, quote);
    return true;
  }

  await ctx.reply(
    `Usage: ${prefix}quote <id|random|search|add|remove> | ${prefix}quote random`
  );
  return true;
}

function getRoleFlags(ctx, settings) {
  if (ctx.platform === "discord") {
    const roles = ctx.meta?.message?.member?.roles?.cache;
    if (!roles) {
      return { isAdmin: false, isMod: false };
    }
    const adminIds = parseList(settings.getSetting("discord_admin_role_id"));
    const modIds = parseList(settings.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 parseList(value) {
  return (value || "")
    .toString()
    .split(/[,\s]+/)
    .map((item) => item.trim())
    .filter(Boolean);
}

function resolveUserIdByUsername(db, username) {
  const desired = (username || "").trim();
  if (!desired) {
    return null;
  }
  const row = db
    .prepare("SELECT id FROM user_profiles WHERE internal_username = ? LIMIT 1")
    .get(desired);
  return row?.id || null;
}

function listQuotes(db, { includeHidden = true, includeArchived = true } = {}) {
  const where = [];
  if (!includeHidden) {
    where.push("hidden = 0");
  }
  if (!includeArchived) {
    where.push("archived = 0");
  }
  const clause = where.length ? `WHERE ${where.join(" AND ")}` : "";
  return db
    .prepare(
      "SELECT id, quote_text, quoter, quoter_user_id, game_name, quote_datetime, edited_by, edited_last, hidden, archived " +
        `FROM quotes ${clause} ORDER BY quote_datetime DESC, id DESC`
    )
    .all()
    .map(normalizeQuoteRow);
}

function getQuoteById(db, id, { includeHidden, includeArchived }) {
  const where = ["id = ?"];
  const params = [id];
  if (!includeHidden) {
    where.push("hidden = 0");
  }
  if (!includeArchived) {
    where.push("archived = 0");
  }
  const row = db
    .prepare(
      `SELECT id, quote_text, quoter, quoter_user_id, game_name, quote_datetime, edited_by, edited_last, hidden, archived FROM quotes WHERE ${where.join(
        " AND "
      )} LIMIT 1`
    )
    .get(...params);
  return row ? normalizeQuoteRow(row) : null;
}

function getRandomQuote(db) {
  const row = db
    .prepare(
      "SELECT id, quote_text, quoter, quoter_user_id, game_name, quote_datetime, edited_by, edited_last, hidden, archived " +
        "FROM quotes WHERE hidden = 0 AND archived = 0 ORDER BY RANDOM() LIMIT 1"
    )
    .get();
  return row ? normalizeQuoteRow(row) : null;
}

function addQuote(db, {
  quoteText,
  quoter,
  quoterUserId,
  gameName,
  quoteDatetime,
  editedBy,
  editedLast,
  hidden,
  archived
}) {
  const result = db
    .prepare(
      "INSERT INTO quotes (quote_text, quoter, quoter_user_id, game_name, quote_datetime, edited_by, edited_last, hidden, archived) " +
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
    )
    .run(
      quoteText,
      quoter,
      quoterUserId || null,
      gameName || null,
      quoteDatetime || Date.now(),
      editedBy || null,
      editedLast || null,
      hidden ? 1 : 0,
      archived ? 1 : 0
    );
  return result.lastInsertRowid;
}

function updateQuote(db, id, {
  quoteText,
  quoter,
  quoterUserId,
  gameName,
  quoteDatetime,
  hidden,
  archived,
  editedBy,
  editedLast
}) {
  let effectiveDatetime = quoteDatetime;
  let effectiveQuoterUserId = quoterUserId;
  if (!effectiveDatetime) {
    const existing = db
      .prepare("SELECT quote_datetime, quoter_user_id FROM quotes WHERE id = ?")
      .get(id);
    effectiveDatetime = existing?.quote_datetime || Date.now();
    if (effectiveQuoterUserId === undefined) {
      effectiveQuoterUserId = existing?.quoter_user_id || null;
    }
  } else if (effectiveQuoterUserId === undefined) {
    const existing = db
      .prepare("SELECT quoter_user_id FROM quotes WHERE id = ?")
      .get(id);
    effectiveQuoterUserId = existing?.quoter_user_id || null;
  }
  const updates = [
    quoteText,
    quoter,
    effectiveQuoterUserId || null,
    gameName || null,
    effectiveDatetime,
    editedBy || null,
    editedLast || null,
    hidden ? 1 : 0,
    archived ? 1 : 0,
    id
  ];
  db.prepare(
    "UPDATE quotes SET quote_text = ?, quoter = ?, quoter_user_id = ?, game_name = ?, quote_datetime = ?, edited_by = ?, edited_last = ?, hidden = ?, archived = ? WHERE id = ?"
  ).run(...updates);
}

function setQuoteHidden(db, id, hidden, editor) {
  const parsed = parseInt(id, 10);
  if (!Number.isFinite(parsed)) {
    return false;
  }
  const result = db
    .prepare(
      "UPDATE quotes SET hidden = ?, edited_by = ?, edited_last = ? WHERE id = ?"
    )
    .run(hidden ? 1 : 0, editor || null, Date.now(), parsed);
  return result.changes > 0;
}

function setQuoteArchived(db, id, archived, editor) {
  const parsed = parseInt(id, 10);
  if (!Number.isFinite(parsed)) {
    return false;
  }
  const result = db
    .prepare(
      "UPDATE quotes SET archived = ?, edited_by = ?, edited_last = ? WHERE id = ?"
    )
    .run(archived ? 1 : 0, editor || null, Date.now(), parsed);
  return result.changes > 0;
}

function searchQuotes(db, searchText, { includeHidden = false } = {}) {
  const term = (searchText || "").trim().toLowerCase();
  if (!term) {
    return null;
  }
  const tokens = term.split(/\s+/).filter(Boolean);
  if (!tokens.length) {
    return null;
  }
  const where = ["archived = 0"];
  if (!includeHidden) {
    where.push("hidden = 0");
  }
  const rows = db
    .prepare(
      "SELECT id, quote_text, quoter, quoter_user_id, game_name, quote_datetime, edited_by, edited_last, hidden, archived " +
        `FROM quotes WHERE ${where.join(" AND ")}`
    )
    .all()
    .map(normalizeQuoteRow);
  let best = null;
  let bestScore = 0;

  for (const row of rows) {
    const hayText = (row.quote_text || "").toLowerCase();
    const hayGame = (row.game_name || "").toLowerCase();
    const hayQuoter = (row.quoter || "").toLowerCase();
    const hayId = row.id.toString();
    const hayDate = buildSearchDate(row.quote_datetime);

    let score = 0;
    let matchesAll = true;

    for (const token of tokens) {
      const matchesText = hayText.includes(token);
      const matchesGame = hayGame.includes(token);
      const matchesQuoter = hayQuoter.includes(token);
      const matchesId = hayId.includes(token);
      const matchesDate = hayDate.includes(token);
      if (!matchesText && !matchesGame && !matchesQuoter && !matchesId && !matchesDate) {
        matchesAll = false;
        break;
      }
      if (matchesText) score += 5;
      if (matchesGame) score += 4;
      if (matchesQuoter) score += 2;
      if (matchesId) score += 3;
      if (matchesDate) score += 1;
    }

    if (!matchesAll) {
      continue;
    }
    if (hayText.includes(term)) {
      score += 6;
    }
    if (score > bestScore) {
      bestScore = score;
      best = row;
    } else if (score === bestScore && best && row.quote_datetime > best.quote_datetime) {
      best = row;
    }
  }

  return best;
}

async function replyWithQuote(ctx, quote) {
  if (ctx.platform === "discord") {
    const embed = buildQuoteEmbed(quote);
    await ctx.reply({ embeds: [embed] });
    return;
  }
  await ctx.reply(buildQuoteText(quote));
}

function buildQuoteText(quote) {
  const dateLabel = formatDateLabel(quote.quote_datetime);
  const quoter = quote.quoter || "Unknown";
  return `#${quote.id} "${quote.quote_text}" - quoted by ${quoter} ${dateLabel}`;
}

function buildQuoteEmbed(quote) {
  const fields = [
    { name: "Quoted by", value: quote.quoter || "Unknown", inline: true }
  ];
  if (quote.game_name) {
    fields.push({ name: "Game", value: quote.game_name, inline: true });
  }
  fields.push({
    name: "Date",
    value: formatDateTime(quote.quote_datetime),
    inline: true
  });
  return {
    title: `Quote #${quote.id}`,
    description: `"${quote.quote_text}"`,
    fields,
    timestamp: new Date(quote.quote_datetime).toISOString()
  };
}

function normalizeQuoteRow(row) {
  return {
    id: row.id,
    quote_text: row.quote_text,
    quoter: row.quoter,
    quoter_user_id: row.quoter_user_id,
    game_name: row.game_name,
    quote_datetime: row.quote_datetime,
    edited_by: row.edited_by,
    edited_last: row.edited_last,
    hidden: Boolean(row.hidden),
    archived: Boolean(row.archived)
  };
}

function formatDateTime(timestamp) {
  if (!timestamp) {
    return "Unknown";
  }
  const date = new Date(timestamp);
  if (Number.isNaN(date.getTime())) {
    return "Unknown";
  }
  return new Intl.DateTimeFormat("en-US", {
    dateStyle: "medium",
    timeStyle: "short"
  }).format(date);
}

function formatDateLabel(timestamp) {
  if (!timestamp) {
    return "";
  }
  const date = new Date(timestamp);
  if (Number.isNaN(date.getTime())) {
    return "";
  }
  return new Intl.DateTimeFormat("en-US", {
    year: "numeric",
    month: "short",
    day: "numeric"
  }).format(date);
}

function formatDateInput(timestamp) {
  if (!timestamp) {
    return "";
  }
  const date = new Date(timestamp);
  if (Number.isNaN(date.getTime())) {
    return "";
  }
  const pad = (value) => value.toString().padStart(2, "0");
  const yyyy = date.getFullYear();
  const mm = pad(date.getMonth() + 1);
  const dd = pad(date.getDate());
  const hh = pad(date.getHours());
  const min = pad(date.getMinutes());
  return `${yyyy}-${mm}-${dd}T${hh}:${min}`;
}

function parseDateInput(value) {
  if (!value) {
    return null;
  }
  const parsed = new Date(value);
  if (Number.isNaN(parsed.getTime())) {
    return null;
  }
  return parsed.getTime();
}

function buildSearchDate(timestamp) {
  if (!timestamp) {
    return "";
  }
  const date = new Date(timestamp);
  if (Number.isNaN(date.getTime())) {
    return "";
  }
  return `${date.toISOString().slice(0, 10)} ${formatDateLabel(timestamp)}`.toLowerCase();
}

async function resolveGameName(ctx, settings) {
  if (ctx.platform !== "twitch") {
    return null;
  }
  const roomId = ctx.meta?.tags?.["room-id"] || ctx.meta?.tags?.roomId;
  if (!roomId) {
    return null;
  }
  const clientId = settings.getSetting("twitch_client_id");
  const clientSecret = settings.getSetting("twitch_client_secret");
  if (!clientId || !clientSecret) {
    return null;
  }
  const token = await getTwitchAppToken(clientId, clientSecret);
  if (!token) {
    return null;
  }
  const response = await fetch(
    `https://api.twitch.tv/helix/channels?broadcaster_id=${encodeURIComponent(roomId)}`,
    {
      headers: {
        "Client-Id": clientId,
        Authorization: `Bearer ${token}`
      }
    }
  );
  if (!response.ok) {
    return null;
  }
  const data = await response.json();
  const channel = data.data?.[0];
  return channel?.game_name || null;
}

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