const path = require("path");
const fs = require("fs");
const crypto = require("crypto");
const multer = require("multer");
const {
  cleanupUploadedFiles,
  safeDownloadFilename,
  validateUploadedFiles
} = require("../../src/services/upload-security");

const PLUGIN_ID = "moderation";
const EVIDENCE_DIR = path.join(__dirname, "..", "..", "data", "moderation", "evidence");
const PRESET_DURATIONS = [
  { label: "1 hour", seconds: 60 * 60 },
  { label: "3 hours", seconds: 3 * 60 * 60 },
  { label: "6 hours", seconds: 6 * 60 * 60 },
  { label: "12 hours", seconds: 12 * 60 * 60 },
  { label: "1 day", seconds: 24 * 60 * 60 },
  { label: "7 days", seconds: 7 * 24 * 60 * 60 },
  { label: "14 days", seconds: 14 * 24 * 60 * 60 },
  { label: "1 month", seconds: 30 * 24 * 60 * 60 },
  { label: "3 months", seconds: 90 * 24 * 60 * 60 },
  { label: "6 months", seconds: 180 * 24 * 60 * 60 },
  { label: "9 months", seconds: 270 * 24 * 60 * 60 },
  { label: "1 year", seconds: 365 * 24 * 60 * 60 }
];

module.exports = {
  id: PLUGIN_ID,
  init({ app, web, db, settings, discordClient, twitchClient, youtubeClient }) {
    ensureTables(db);
    ensureBanPot(db);
    ensureEvidenceDir();

    const upload = multer({
      storage: multer.diskStorage({
        destination: (_req, _file, cb) => cb(null, EVIDENCE_DIR),
        filename: (_req, file, cb) => {
          const ext = path.extname(file.originalname || ".png").slice(0, 10);
          cb(null, `${crypto.randomUUID()}${ext}`);
        }
      }),
      limits: { fileSize: 8 * 1024 * 1024, files: 4, fields: 40, parts: 45 },
      fileFilter: (_req, file, cb) => {
        if (["image/png", "image/jpeg", "image/webp"].includes(file.mimetype)) return cb(null, true);
        cb(new Error("Evidence must be a PNG, JPEG, or WebP image."));
      }
    });
    const evidenceUpload = (req, res, next) => upload.array("evidence_files", 4)(req, res, (error) => {
      if (error) {
        cleanupUploadedFiles(req.files);
        req.files = [];
        req.evidenceUploadError = error.code === "LIMIT_FILE_SIZE"
          ? "Each evidence image must be 8 MB or smaller."
          : error.message;
        return next();
      }
      try {
        validateUploadedFiles(req.files, {
          allowedTypes: ["png", "jpeg", "webp"],
          maxBytes: 8 * 1024 * 1024,
          requireExtension: true,
          label: "evidence image"
        });
      } catch (validationError) {
        cleanupUploadedFiles(req.files);
        req.files = [];
        req.evidenceUploadError = validationError.message;
      }
      next();
    });

    installGlobalGate(app, (req, res, next) => {
      if (!req.session?.user) {
        return next();
      }
      if (req.path.startsWith("/auth")) {
        return next();
      }
      if (req.path.startsWith("/moderation/status")) {
        return next();
      }
      linkSubjectToUser(db, req.session.user.id);
      const sanction = getActiveSanctionForUser(db, req.session.user.id);
      if (!sanction) {
        return next();
      }
      res.status(403).render(path.join(__dirname, "views", "status.ejs"), {
        title: "Account restricted",
        sanction
      });
    });

    const router = web.createRouter();

    router.get("/status", (req, res) => {
      if (!req.session?.user) {
        return res.redirect("/");
      }
      linkSubjectToUser(db, req.session.user.id);
      const sanction = getActiveSanctionForUser(db, req.session.user.id);
      if (!sanction) {
        return res.redirect("/");
      }
      res.status(403).render(path.join(__dirname, "views", "status.ejs"), {
        title: "Account restricted",
        sanction
      });
    });

    router.get("/", (req, res) => {
      if (!req.session.user) {
        return res.redirect("/");
      }
      const isAdmin = Boolean(req.session.user?.isAdmin);
      const isMod = Boolean(req.session.user?.isAdmin || req.session.user?.isMod);
      const actions = listActions(db, { limit: 500 });
      const actionEvidence = listEvidenceForActions(
        db,
        actions.map((action) => action.id)
      );
      const notes = listNotes(db, { limit: 1000 });
      const activeSanctions = listActiveSanctions(db);
      const banPot = getBanPot(db);
      res.render(path.join(__dirname, "views", "moderation.ejs"), {
        title: "Moderation Center",
        isAdmin,
        isMod,
        actions,
        actionEvidence,
        notes,
        activeSanctions,
        banPot,
        presets: PRESET_DURATIONS
      });
    });

    router.get("/tos-bans", (req, res) => {
      if (!req.session.user) {
        return res.redirect("/");
      }
      const isAdmin = Boolean(req.session.user?.isAdmin);
      const isMod = Boolean(req.session.user?.isAdmin || req.session.user?.isMod);
      if (!isMod) {
        return deny(res);
      }
      const actions = listActions(db, { limit: 500 });
      const actionEvidence = listEvidenceForActions(
        db,
        actions.map((action) => action.id)
      );
      const activeSanctions = listActiveSanctions(db);
      res.render(path.join(__dirname, "views", "tos-bans.ejs"), {
        title: "TOs & Bans",
        isAdmin,
        isMod,
        actions,
        actionEvidence,
        activeSanctions,
        presets: PRESET_DURATIONS
      });
    });

    router.get("/evidence/:id", (req, res) => {
      if (!req.session.user) {
        return res.redirect("/");
      }
      const isMod = Boolean(req.session.user?.isAdmin || req.session.user?.isMod);
      if (!isMod) {
        return deny(res);
      }
      const row = db
        .prepare("SELECT file_path, file_name FROM moderation_evidence WHERE id = ?")
        .get(req.params.id);
      if (!row?.file_path) {
        return res.status(404).render("error", {
          title: "Not found",
          message: "Evidence file not found."
        });
      }
      const evidencePath = path.resolve(row.file_path);
      const evidenceRoot = `${path.resolve(EVIDENCE_DIR)}${path.sep}`;
      if (!evidencePath.startsWith(evidenceRoot) || !fs.existsSync(evidencePath)) {
        return res.status(404).render("error", {
          title: "Not found",
          message: "Evidence file not found."
        });
      }
      res.download(evidencePath, safeDownloadFilename(row.file_name, "evidence"));
    });

    router.post("/actions", (req, res, next) => {
      if (!req.session.user) {
        return res.redirect("/");
      }
      const isMod = Boolean(req.session.user?.isAdmin || req.session.user?.isMod);
      if (!isMod) {
        return deny(res);
      }
      next();
    }, evidenceUpload, async (req, res) => {
      const isAdmin = Boolean(req.session.user?.isAdmin);
      const isMod = Boolean(req.session.user?.isAdmin || req.session.user?.isMod);
      if (req.evidenceUploadError) {
        req.session.flash = { type: "error", message: req.evidenceUploadError };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }

      const actionType = (req.body.action_type || "").toLowerCase();
      if (actionType === "kick") {
        cleanupUploadedFiles(req.files);
        req.session.flash = {
          type: "info",
          message: "Kick actions are coming soon."
        };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }

      const target = resolveTarget(db, req.body);
      if (!target) {
        cleanupUploadedFiles(req.files);
        req.session.flash = { type: "error", message: "Target not found." };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }

      const reasonShort = (req.body.reason_short || "").trim();
      const reasonDetail = (req.body.reason_detail || "").trim();
      if (!reasonShort || !reasonDetail) {
        cleanupUploadedFiles(req.files);
        req.session.flash = {
          type: "error",
          message: "Both summary and detailed reasons are required."
        };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }

      const durationSeconds =
        actionType === "timeout"
          ? buildDurationSeconds(req.body, isAdmin, isMod)
          : null;
      const createdBy = req.session.user.username || "Moderator";
      const createdById = req.session.user.id;
      const action = createAction(db, {
        subjectId: target.subjectId,
        actionType,
        scope: "global",
        platform: "global",
        reasonShort,
        reasonDetail,
        durationSeconds,
        createdById,
        createdByName: createdBy,
        source: "manual"
      });

      const evidenceFiles = (req.files || []).map((file) => ({
        path: file.path,
        name: file.originalname
      }));
      evidenceFiles.forEach((file) => {
        addEvidence(db, action.id, file.path, file.name, createdById);
      });

      const identities = listSubjectIdentities(db, target.subjectId);
      await enforceAction({
        action,
        identities,
        settings,
        discordClient,
        twitchClient,
        youtubeClient,
        reasonShort,
        reasonDetail
      });

      if (actionType === "ban") {
        distributeBanAssets(db, target.subjectId, { reason: reasonShort });
      }

      req.session.flash = { type: "success", message: "Moderation action recorded." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/actions/:id/update-timeout", (req, res) => {
      if (!req.session.user) {
        return res.redirect("/");
      }
      const isMod = Boolean(req.session.user?.isAdmin || req.session.user?.isMod);
      if (!isMod) {
        return deny(res);
      }
      const action = getAction(db, req.params.id);
      if (!action || action.action_type !== "timeout") {
        req.session.flash = { type: "error", message: "Timeout not found." };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      const durationSeconds = buildDurationSeconds(req.body, req.session.user?.isAdmin, true);
      const expiresAt = durationSeconds ? Date.now() + durationSeconds * 1000 : null;
      updateActionDuration(db, action.id, durationSeconds, expiresAt);
      req.session.flash = { type: "success", message: "Timeout updated." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/actions/:id/revoke", async (req, res) => {
      if (!req.session.user) {
        return res.redirect("/");
      }
      const action = getAction(db, req.params.id);
      if (!action) {
        req.session.flash = { type: "error", message: "Action not found." };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      const isAdmin = Boolean(req.session.user?.isAdmin);
      const isMod = Boolean(req.session.user?.isAdmin || req.session.user?.isMod);
      if (action.action_type === "ban" && !isAdmin) {
        return deny(res);
      }
      if (action.action_type === "timeout" && !isMod) {
        return deny(res);
      }
      setActionStatus(db, action.id, "revoked");

      const identities = listSubjectIdentities(db, action.subject_id);
      await revokeAction({
        action,
        identities,
        settings,
        discordClient,
        twitchClient,
        youtubeClient
      });

      req.session.flash = { type: "success", message: "Action revoked." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/notes", (req, res) => {
      if (!req.session.user) {
        return res.redirect("/");
      }
      const isMod = Boolean(req.session.user?.isAdmin || req.session.user?.isMod);
      if (!isMod) {
        return deny(res);
      }
      const target = resolveTarget(db, req.body);
      const note = (req.body.note || "").trim();
      if (!target || !note) {
        req.session.flash = { type: "error", message: "Target and note are required." };
        return res.redirect(`/plugins/${PLUGIN_ID}`);
      }
      addNote(db, target.subjectId, note, req.session.user.id, req.session.user.username || "Moderator");
      req.session.flash = { type: "success", message: "Note added." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

    router.post("/notes/:id/delete", (req, res) => {
      if (!req.session.user) {
        return res.redirect("/");
      }
      if (!req.session.user?.isAdmin) {
        return deny(res);
      }
      const deleted = deleteNote(db, req.params.id);
      req.session.flash = deleted
        ? { type: "success", message: "Note deleted." }
        : { type: "error", message: "Note not found." };
      res.redirect(`/plugins/${PLUGIN_ID}`);
    });

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

    web.addNavItem({
      label: "TOs & Bans",
      path: `/plugins/${PLUGIN_ID}/tos-bans`,
      role: "mod",
      section: "moderation"
    });

    if (discordClient) {
      startDiscordAuditPolling(db, settings, discordClient);
    }
    if (twitchClient) {
      attachTwitchModerationEvents(db, twitchClient);
    }
    installFreezeHook(db);
  }
};

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

function ensureEvidenceDir() {
  fs.mkdirSync(EVIDENCE_DIR, { recursive: true });
}

function ensureTables(db) {
  db.exec(`
    CREATE TABLE IF NOT EXISTS moderation_subjects (
      id TEXT PRIMARY KEY,
      internal_user_id TEXT,
      display_name TEXT,
      created_at INTEGER NOT NULL
    );

    CREATE TABLE IF NOT EXISTS moderation_identities (
      id TEXT PRIMARY KEY,
      subject_id TEXT NOT NULL,
      platform TEXT NOT NULL,
      platform_user_id TEXT,
      platform_username TEXT,
      created_at INTEGER NOT NULL,
      UNIQUE(platform, platform_user_id)
    );

    CREATE TABLE IF NOT EXISTS moderation_actions (
      id TEXT PRIMARY KEY,
      subject_id TEXT NOT NULL,
      action_type TEXT NOT NULL,
      scope TEXT NOT NULL,
      platform TEXT,
      source TEXT,
      status TEXT NOT NULL,
      duration_seconds INTEGER,
      reason_short TEXT NOT NULL,
      reason_detail TEXT NOT NULL,
      created_by_user_id TEXT,
      created_by_name TEXT,
      created_at INTEGER NOT NULL,
      expires_at INTEGER,
      external_ref TEXT
    );

    CREATE TABLE IF NOT EXISTS moderation_evidence (
      id TEXT PRIMARY KEY,
      action_id TEXT NOT NULL,
      file_path TEXT NOT NULL,
      file_name TEXT,
      uploaded_by TEXT,
      created_at INTEGER NOT NULL
    );

    CREATE TABLE IF NOT EXISTS moderation_notes (
      id TEXT PRIMARY KEY,
      subject_id TEXT NOT NULL,
      note TEXT NOT NULL,
      created_by_user_id TEXT,
      created_by_name TEXT,
      created_at INTEGER NOT NULL
    );

    CREATE TABLE IF NOT EXISTS moderation_ban_pot (
      id INTEGER PRIMARY KEY CHECK (id = 1),
      balance INTEGER NOT NULL DEFAULT 0,
      updated_at INTEGER NOT NULL
    );
  `);
}

function ensureBanPot(db) {
  const existing = db.prepare("SELECT id FROM moderation_ban_pot WHERE id = 1").get();
  if (!existing) {
    db.prepare("INSERT INTO moderation_ban_pot (id, balance, updated_at) VALUES (1, 0, ?)")
      .run(Date.now());
  }
}

function resolveTarget(db, body) {
  const selectedUserId = (body.target_user_id || "").trim();
  if (selectedUserId) {
    const user = db
      .prepare("SELECT id, internal_username FROM user_profiles WHERE id = ?")
      .get(selectedUserId);
    if (user) {
      const subjectId = getOrCreateSubjectByUser(db, user.id, user.internal_username);
      syncSubjectIdentities(db, subjectId, user.id);
      return { subjectId, internalUserId: user.id };
    }
  }
  const internal = (body.target_username || "").trim();
  if (internal) {
    const user = db
      .prepare("SELECT id, internal_username FROM user_profiles WHERE internal_username = ?")
      .get(internal);
    if (user) {
      const subjectId = getOrCreateSubjectByUser(db, user.id, user.internal_username);
      syncSubjectIdentities(db, subjectId, user.id);
      return { subjectId, internalUserId: user.id };
    }
  }

  const platform = (body.target_platform || "").trim().toLowerCase();
  const platformId = (body.target_platform_id || "").trim();
  const platformUsername = (body.target_platform_username || "").trim();
  if (!platform) {
    return null;
  }
  const key = platformId || platformUsername;
  if (!key) {
    return null;
  }
  const subjectId = getOrCreateSubjectByIdentity(
    db,
    platform,
    key,
    platformUsername || platformId
  );
  return { subjectId, internalUserId: null };
}

function getOrCreateSubjectByUser(db, userId, displayName) {
  const existing = db
    .prepare("SELECT id FROM moderation_subjects WHERE internal_user_id = ?")
    .get(userId);
  if (existing) {
    return existing.id;
  }
  const id = crypto.randomUUID();
  db.prepare(
    "INSERT INTO moderation_subjects (id, internal_user_id, display_name, created_at) VALUES (?, ?, ?, ?)"
  ).run(id, userId, displayName || null, Date.now());
  return id;
}

function getOrCreateSubjectByIdentity(db, platform, platformUserId, platformUsername) {
  const existing = db
    .prepare("SELECT subject_id FROM moderation_identities WHERE platform = ? AND platform_user_id = ?")
    .get(platform, platformUserId);
  if (existing) {
    return existing.subject_id;
  }
  const subjectId = crypto.randomUUID();
  db.prepare(
    "INSERT INTO moderation_subjects (id, internal_user_id, display_name, created_at) VALUES (?, NULL, ?, ?)"
  ).run(subjectId, platformUsername || platformUserId, Date.now());
  db.prepare(
    "INSERT INTO moderation_identities (id, subject_id, platform, platform_user_id, platform_username, created_at) VALUES (?, ?, ?, ?, ?, ?)"
  ).run(crypto.randomUUID(), subjectId, platform, platformUserId, platformUsername || null, Date.now());
  return subjectId;
}

function syncSubjectIdentities(db, subjectId, userId) {
  const identities = db
    .prepare("SELECT provider, provider_user_id, display_name FROM user_identities WHERE user_id = ?")
    .all(userId);
  identities.forEach((identity) => {
    const existing = db
      .prepare(
        "SELECT id FROM moderation_identities WHERE platform = ? AND platform_user_id = ?"
      )
      .get(identity.provider, identity.provider_user_id);
    if (existing) {
      return;
    }
    db.prepare(
      "INSERT INTO moderation_identities (id, subject_id, platform, platform_user_id, platform_username, created_at) VALUES (?, ?, ?, ?, ?, ?)"
    ).run(
      crypto.randomUUID(),
      subjectId,
      identity.provider,
      identity.provider_user_id,
      identity.display_name || identity.provider_user_id,
      Date.now()
    );
  });
}

function linkSubjectToUser(db, userId) {
  const subject = db
    .prepare("SELECT id FROM moderation_subjects WHERE internal_user_id = ?")
    .get(userId);
  if (subject) {
    return subject.id;
  }
  const identities = db
    .prepare("SELECT provider, provider_user_id, display_name FROM user_identities WHERE user_id = ?")
    .all(userId);
  for (const identity of identities) {
    const existing = db
      .prepare(
        "SELECT subject_id FROM moderation_identities WHERE platform = ? AND platform_user_id = ?"
      )
      .get(identity.provider, identity.provider_user_id);
    if (existing) {
      db.prepare(
        "UPDATE moderation_subjects SET internal_user_id = ?, display_name = COALESCE(display_name, ?) WHERE id = ?"
      ).run(userId, identity.display_name || identity.provider_user_id, existing.subject_id);
      return existing.subject_id;
    }
  }
  return null;
}

function listSubjectIdentities(db, subjectId) {
  return db
    .prepare(
      "SELECT platform, platform_user_id, platform_username FROM moderation_identities WHERE subject_id = ?"
    )
    .all(subjectId);
}

function buildDurationSeconds(body, isAdmin, isMod) {
  if (body.permanent === "on") {
    return null;
  }
  if (isAdmin && body.duration_value) {
    const value = Number(body.duration_value);
    const unit = (body.duration_unit || "hours").toLowerCase();
    if (Number.isFinite(value) && value > 0) {
      const multipliers = {
        hour: 3600,
        hours: 3600,
        day: 86400,
        days: 86400,
        week: 604800,
        weeks: 604800,
        month: 2592000,
        months: 2592000,
        year: 31536000,
        years: 31536000
      };
      const multiplier = multipliers[unit] || 3600;
      return Math.floor(value * multiplier);
    }
  }
  if (isMod && body.duration_preset) {
    const preset = PRESET_DURATIONS.find(
      (entry) => entry.seconds.toString() === body.duration_preset.toString()
    );
    return preset ? preset.seconds : null;
  }
  return null;
}

function createAction(db, payload) {
  if (payload.externalRef) {
    const existing = db
      .prepare("SELECT id FROM moderation_actions WHERE external_ref = ?")
      .get(payload.externalRef);
    if (existing) {
      return getAction(db, existing.id);
    }
  }
  const now = Date.now();
  const expiresAt = payload.durationSeconds ? now + payload.durationSeconds * 1000 : null;
  const action = {
    id: crypto.randomUUID(),
    subject_id: payload.subjectId,
    action_type: payload.actionType,
    scope: payload.scope || "global",
    platform: payload.platform || "global",
    source: payload.source || "manual",
    status: "active",
    duration_seconds: payload.durationSeconds || null,
    reason_short: payload.reasonShort,
    reason_detail: payload.reasonDetail,
    created_by_user_id: payload.createdById || null,
    created_by_name: payload.createdByName || null,
    created_at: now,
    expires_at: expiresAt,
    external_ref: payload.externalRef || null
  };
  db.prepare(
    "INSERT INTO moderation_actions (id, subject_id, action_type, scope, platform, source, status, duration_seconds, reason_short, reason_detail, created_by_user_id, created_by_name, created_at, expires_at, external_ref) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
  ).run(
    action.id,
    action.subject_id,
    action.action_type,
    action.scope,
    action.platform,
    action.source,
    action.status,
    action.duration_seconds,
    action.reason_short,
    action.reason_detail,
    action.created_by_user_id,
    action.created_by_name,
    action.created_at,
    action.expires_at,
    action.external_ref
  );
  return action;
}

function addEvidence(db, actionId, filePath, fileName, uploadedBy) {
  db.prepare(
    "INSERT INTO moderation_evidence (id, action_id, file_path, file_name, uploaded_by, created_at) VALUES (?, ?, ?, ?, ?, ?)"
  ).run(crypto.randomUUID(), actionId, filePath, fileName, uploadedBy, Date.now());
}

function addNote(db, subjectId, note, createdById, createdByName) {
  db.prepare(
    "INSERT INTO moderation_notes (id, subject_id, note, created_by_user_id, created_by_name, created_at) VALUES (?, ?, ?, ?, ?, ?)"
  ).run(crypto.randomUUID(), subjectId, note, createdById, createdByName, Date.now());
}

function deleteNote(db, id) {
  const result = db.prepare("DELETE FROM moderation_notes WHERE id = ?").run(id);
  return result.changes > 0;
}

function listActions(db, { limit = 200 } = {}) {
  return db
    .prepare(
      "SELECT a.*, s.display_name FROM moderation_actions a " +
        "LEFT JOIN moderation_subjects s ON s.id = a.subject_id " +
        "ORDER BY a.created_at DESC LIMIT ?"
    )
    .all(limit);
}

function listEvidenceForActions(db, ids) {
  if (!ids || !ids.length) {
    return {};
  }
  const rows = db
    .prepare(
      `SELECT id, action_id, file_name, file_path FROM moderation_evidence WHERE action_id IN (${ids
        .map(() => "?")
        .join(",")})`
    )
    .all(...ids);
  return rows.reduce((acc, row) => {
    if (!acc[row.action_id]) {
      acc[row.action_id] = [];
    }
    acc[row.action_id].push({
      id: row.id,
      name: row.file_name || path.basename(row.file_path)
    });
    return acc;
  }, {});
}

function listNotes(db, { limit = 200 } = {}) {
  return db
    .prepare(
      "SELECT n.*, s.display_name, s.internal_user_id FROM moderation_notes n " +
        "LEFT JOIN moderation_subjects s ON s.id = n.subject_id " +
        "ORDER BY n.created_at DESC LIMIT ?"
    )
    .all(limit);
}

function listActiveSanctions(db) {
  const now = Date.now();
  return db
    .prepare(
      "SELECT a.*, s.display_name FROM moderation_actions a " +
        "LEFT JOIN moderation_subjects s ON s.id = a.subject_id " +
        "WHERE a.status = 'active' AND a.action_type IN ('ban', 'timeout') " +
        "AND (a.expires_at IS NULL OR a.expires_at > ?) " +
        "ORDER BY a.created_at DESC"
    )
    .all(now);
}

function getAction(db, id) {
  return db
    .prepare("SELECT * FROM moderation_actions WHERE id = ?")
    .get(id);
}

function updateActionDuration(db, id, durationSeconds, expiresAt) {
  db.prepare(
    "UPDATE moderation_actions SET duration_seconds = ?, expires_at = ? WHERE id = ?"
  ).run(durationSeconds, expiresAt, id);
}

function setActionStatus(db, id, status) {
  db.prepare("UPDATE moderation_actions SET status = ? WHERE id = ?").run(status, id);
}

function getActiveSanctionForUser(db, userId) {
  const subject = db
    .prepare("SELECT id FROM moderation_subjects WHERE internal_user_id = ?")
    .get(userId);
  if (!subject) {
    return null;
  }
  const now = Date.now();
  const action = db
    .prepare(
      "SELECT a.*, s.display_name FROM moderation_actions a " +
        "LEFT JOIN moderation_subjects s ON s.id = a.subject_id " +
        "WHERE a.subject_id = ? AND a.status = 'active' AND a.action_type IN ('ban', 'timeout') " +
        "AND (a.expires_at IS NULL OR a.expires_at > ?) " +
        "ORDER BY a.created_at DESC LIMIT 1"
    )
    .get(subject.id, now);
  return action || null;
}

function getBanPot(db) {
  const row = db.prepare("SELECT balance FROM moderation_ban_pot WHERE id = 1").get();
  return row ? row.balance : 0;
}

function addBanPot(db, amount) {
  const current = getBanPot(db);
  db.prepare("UPDATE moderation_ban_pot SET balance = ?, updated_at = ? WHERE id = 1").run(
    current + amount,
    Date.now()
  );
}

async function enforceAction({
  action,
  identities,
  settings,
  discordClient,
  twitchClient,
  youtubeClient,
  reasonShort,
  reasonDetail
}) {
  const summary = reasonShort || "Moderation action";
  const detail = reasonDetail || "";
  const duration = action.duration_seconds || null;

  for (const identity of identities) {
    if (identity.platform === "discord") {
      await enforceDiscord(
        discordClient,
        settings,
        identity.platform_user_id,
        action.action_type,
        summary,
        detail,
        duration
      );
    }
    if (identity.platform === "twitch") {
      await enforceTwitch(
        twitchClient,
        identity.platform_username || identity.platform_user_id,
        action.action_type,
        summary,
        duration
      );
    }
    if (identity.platform === "youtube") {
      // Placeholder for YouTube enforcement
      // Future: apply chat bans/timeouts with YouTube API
      continue;
    }
  }
}

async function revokeAction({ action, identities, settings, discordClient, twitchClient }) {
  for (const identity of identities) {
    if (identity.platform === "discord") {
      if (action.action_type === "ban") {
        await revokeDiscordBan(discordClient, settings, identity.platform_user_id);
      }
      if (action.action_type === "timeout") {
        await revokeDiscordTimeout(discordClient, settings, identity.platform_user_id);
      }
    }
    if (identity.platform === "twitch") {
      if (action.action_type === "ban") {
        await revokeTwitchBan(twitchClient, identity.platform_username || identity.platform_user_id);
      }
      if (action.action_type === "timeout") {
        await revokeTwitchTimeout(twitchClient, identity.platform_username || identity.platform_user_id);
      }
    }
  }
}

async function enforceDiscord(client, settings, userId, actionType, reasonShort, reasonDetail, durationSeconds) {
  if (!client || !userId) {
    return;
  }
  const guildId = settings?.getSetting?.("discord_guild_id", null);
  if (!guildId) {
    return;
  }
  const guild = client.guilds?.cache?.get(guildId) || null;
  if (!guild) {
    return;
  }
  const reason = `${reasonShort}${reasonDetail ? ` | ${reasonDetail}` : ""}`.slice(0, 480);
  if (actionType === "ban") {
    await guild.members.ban(userId, { reason }).catch(() => null);
    await notifyDiscordMember(guild, userId, reasonShort, reasonDetail, "ban");
    return;
  }
  if (actionType === "timeout") {
    const member = await guild.members.fetch(userId).catch(() => null);
    if (!member) {
      return;
    }
    const durationMs = durationSeconds ? durationSeconds * 1000 : null;
    if (durationMs) {
      await member.timeout(durationMs, reason).catch(() => null);
      await notifyDiscordMember(guild, userId, reasonShort, reasonDetail, "timeout");
    }
  }
}

async function revokeDiscordBan(client, settings, userId) {
  if (!client || !userId) {
    return;
  }
  const guildId = settings?.getSetting?.("discord_guild_id", null);
  if (!guildId) {
    return;
  }
  const guild = client.guilds?.cache?.get(guildId) || null;
  if (!guild) {
    return;
  }
  await guild.members.unban(userId).catch(() => null);
}

async function revokeDiscordTimeout(client, settings, userId) {
  if (!client || !userId) {
    return;
  }
  const guildId = settings?.getSetting?.("discord_guild_id", null);
  const guild = client.guilds?.cache?.get(guildId) || null;
  if (!guild) {
    return;
  }
  const member = await guild.members.fetch(userId).catch(() => null);
  if (!member) {
    return;
  }
  await member.timeout(null).catch(() => null);
}

async function notifyDiscordMember(guild, userId, reasonShort, reasonDetail, type) {
  const member = await guild.members.fetch(userId).catch(() => null);
  if (!member) {
    return;
  }
  const title = type === "ban" ? "You have been banned" : "You have been timed out";
  const message = `${title} from ${guild.name}.\nSummary: ${reasonShort}\nDetails: ${reasonDetail}`.slice(0, 1900);
  await member.send(message).catch(() => null);
}

async function enforceTwitch(client, username, actionType, reasonShort, durationSeconds) {
  if (!client || !username) {
    return;
  }
  const reason = reasonShort || "Moderation action";
  const channels = typeof client.getChannels === "function" ? client.getChannels() : [];
  const channel = channels[0] || null;
  if (!channel) {
    return;
  }
  if (actionType === "ban") {
    await client.ban(channel, username, reason).catch(() => null);
    await tryWhisper(client, username, `You have been banned. Reason: ${reason}`);
    return;
  }
  if (actionType === "timeout") {
    const duration = durationSeconds || 3600;
    await client.timeout(channel, username, duration, reason).catch(() => null);
    await tryWhisper(client, username, `You have been timed out. Reason: ${reason}`);
  }
}

async function revokeTwitchBan(client, username) {
  if (!client || !username) {
    return;
  }
  const channels = typeof client.getChannels === "function" ? client.getChannels() : [];
  const channel = channels[0] || null;
  if (!channel) {
    return;
  }
  await client.unban(channel, username).catch(() => null);
}

async function revokeTwitchTimeout(client, username) {
  if (!client || !username) {
    return;
  }
  const channels = typeof client.getChannels === "function" ? client.getChannels() : [];
  const channel = channels[0] || null;
  if (!channel) {
    return;
  }
  await client.unban(channel, username).catch(() => null);
}

async function tryWhisper(client, username, message) {
  if (!client || typeof client.whisper !== "function") {
    return;
  }
  await client.whisper(username, message).catch(() => null);
}

function distributeBanAssets(db, subjectId, { reason }) {
  const framework = global.lumiFrameworks?.economy;
  if (!framework) {
    return;
  }
  const subject = db
    .prepare("SELECT internal_user_id FROM moderation_subjects WHERE id = ?")
    .get(subjectId);
  if (!subject?.internal_user_id) {
    return;
  }
  const balance = framework.getBalance(subject.internal_user_id);
  if (!balance || balance <= 0) {
    return;
  }
  const result = framework.removeBalance({
    userId: subject.internal_user_id,
    amount: balance,
    note: `Ban distribution${reason ? `: ${reason}` : ""}`,
    meta: { source: "moderation", type: "ban" },
    allowFrozen: true
  });
  if (result?.ok === false) {
    return;
  }
  addBanPot(db, balance);
}

function installFreezeHook(db) {
  global.lumiModeration = {
    isFrozen: (userId) => isUserFrozen(db, userId),
    getBanPot: () => getBanPot(db)
  };
}

function isUserFrozen(db, userId) {
  const sanction = getActiveSanctionForUser(db, userId);
  if (!sanction) {
    return false;
  }
  return true;
}

function startDiscordAuditPolling(db, settings, client) {
  const poll = async () => {
    if (!client?.guilds?.cache) {
      return;
    }
    for (const guild of client.guilds.cache.values()) {
      await harvestDiscordAuditLogs(db, guild, settings);
    }
  };
  poll();
  setInterval(poll, 60000);
}

async function harvestDiscordAuditLogs(db, guild, settings) {
  const types = [
    "MEMBER_BAN_ADD",
    "MEMBER_BAN_REMOVE",
    "MEMBER_KICK",
    "MEMBER_UPDATE"
  ];
  for (const type of types) {
    const entries = await guild.fetchAuditLogs({ type, limit: 10 }).catch(() => null);
    if (!entries) {
      continue;
    }
    const lastKey = `discord_audit_${guild.id}_${type}`;
    const lastIdRow = db.prepare("SELECT value FROM plugin_settings WHERE plugin_id = ? AND key = ?")
      .get(PLUGIN_ID, lastKey);
    const lastId = lastIdRow?.value || null;
    const items = Array.from(entries.entries.values());
    for (const entry of items) {
      if (lastId && entry.id === lastId) {
        break;
      }
      const target = entry.target;
      if (!target?.id) {
        continue;
      }
      const subjectId = getOrCreateSubjectByIdentity(db, "discord", target.id, target.tag || target.username);
      const actionType = mapDiscordAuditType(entry, type);
      if (!actionType) {
        continue;
      }
      const duration = actionType === "timeout" ? computeTimeoutDuration(entry) : null;
      createAction(db, {
        subjectId,
        actionType,
        scope: "global",
        platform: "discord",
        reasonShort: entry.reason || "Discord moderation action",
        reasonDetail: entry.reason || "",
        durationSeconds: duration,
        createdById: entry.executor?.id || null,
        createdByName: entry.executor?.tag || entry.executor?.username || null,
        source: "external",
        externalRef: entry.id
      });
    }
    if (items[0]) {
      setPluginSetting(db, lastKey, items[0].id);
    }
  }
}

function mapDiscordAuditType(entry, type) {
  if (type === "MEMBER_BAN_ADD") {
    return "ban";
  }
  if (type === "MEMBER_BAN_REMOVE") {
    return "unban";
  }
  if (type === "MEMBER_KICK") {
    return "kick";
  }
  if (type === "MEMBER_UPDATE") {
    const change = entry.changes?.find((item) => item.key === "communication_disabled_until");
    if (!change) {
      return null;
    }
    if (change.new) {
      return "timeout";
    }
    return "untimeout";
  }
  return null;
}

function computeTimeoutDuration(entry) {
  const change = entry.changes?.find((item) => item.key === "communication_disabled_until");
  if (!change || !change.new) {
    return null;
  }
  const until = new Date(change.new).getTime();
  const now = entry.createdTimestamp || Date.now();
  const diff = Math.max(0, Math.floor((until - now) / 1000));
  return diff || null;
}

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 attachTwitchModerationEvents(db, client) {
  client.on("ban", (channel, username, reason, userstate) => {
    const targetId = userstate?.["target-user-id"] || username;
    const subjectId = getOrCreateSubjectByIdentity(db, "twitch", targetId, username);
    createAction(db, {
      subjectId,
      actionType: "ban",
      scope: "global",
      platform: "twitch",
      reasonShort: reason || "Twitch ban",
      reasonDetail: reason || "",
      durationSeconds: null,
      createdById: userstate?.["room-id"] || null,
      createdByName: userstate?.["display-name"] || null,
      source: "external",
      externalRef: `${channel}:${username}:${Date.now()}`
    });
  });

  client.on("timeout", (channel, username, reason, duration, userstate) => {
    const targetId = userstate?.["target-user-id"] || username;
    const subjectId = getOrCreateSubjectByIdentity(db, "twitch", targetId, username);
    createAction(db, {
      subjectId,
      actionType: "timeout",
      scope: "global",
      platform: "twitch",
      reasonShort: reason || "Twitch timeout",
      reasonDetail: reason || "",
      durationSeconds: duration || null,
      createdById: userstate?.["room-id"] || null,
      createdByName: userstate?.["display-name"] || null,
      source: "external",
      externalRef: `${channel}:${username}:${Date.now()}`
    });
  });
}

function installGlobalGate(app, middleware) {
  app.use(middleware);
  const stack = app._router?.stack;
  if (Array.isArray(stack) && stack.length) {
    const layer = stack.pop();
    stack.unshift(layer);
  }
}
