const rateLimit = require('express-rate-limit')

const log = require('../utils/logger')('ratelimit')

function makeLimiter({ windowMs, max, label, message, keyGenerator, validate }) {
  return rateLimit({
    windowMs,
    max,
    standardHeaders: true,
    legacyHeaders: false,
    message: { message },
    // Default key is the client IP; callers can widen it (e.g. IP + provider).
    ...(keyGenerator ? { keyGenerator } : {}),
    // Custom keyGenerators that fold in req.ip trip v7's IPv6 fallback validator;
    // callers pass `validate` to scope that off just for their limiter.
    ...(validate !== undefined ? { validate } : {}),
    handler: (req, res, next, options) => {
      log.warn(`${label} rate limit exceeded`, { ip: req.ip, path: req.originalUrl })
      res.status(options.statusCode).json(options.message)
    },
  })
}

// Brute-force protection on login.
const loginLimiter = makeLimiter({
  windowMs: 15 * 60 * 1000,
  max: 10,
  label: 'login',
  message: 'Too many login attempts. Please try again later.',
})

// Public self-registration. Mirrors the login cap: a handful of legitimate
// attempts per window, a flood is abuse. The global botScore guard + honeypot
// cover the rest.
const registerLimiter = makeLimiter({
  windowMs: 15 * 60 * 1000,
  max: 10,
  label: 'register',
  message: 'Too many registration attempts. Please try again later.',
})

// Authenticated self-service credential changes (username / password). Tighter
// than login — a signed-in player rarely changes these, and the wrong-current-
// password path also feeds the shared login backoff (see the controller).
const accountChangeLimiter = makeLimiter({
  windowMs: 15 * 60 * 1000,
  max: 10,
  label: 'account-change',
  message: 'Too many changes. Please try again later.',
})

// Throttle the public contact form.
const contactLimiter = makeLimiter({
  windowMs: 60 * 60 * 1000,
  max: 5,
  label: 'contact',
  message: 'Too many messages sent. Please try again later.',
})

// Cap mobile refresh-token exchanges per IP. Legitimate apps refresh at most a
// handful of times per window; a flood is either a bug or an attempt to brute
// the refresh endpoint.
const mobileRefreshLimiter = makeLimiter({
  windowMs: 15 * 60 * 1000,
  max: 30,
  label: 'mobile-refresh',
  message: 'Too many refresh attempts. Please try again later.',
})

// Throttle SSO redirect starts per IP — cheap to trigger, and a flood is either a
// bug or an attempt to spin the OAuth flow. Generous enough for real users.
const ssoStartLimiter = makeLimiter({
  windowMs: 15 * 60 * 1000,
  max: 30,
  label: 'sso-start',
  message: 'Too many sign-in attempts. Please try again later.',
})

// Mobile SSO bridge — throttle /start per IP AND per provider: each call spawns a
// mobile_auth_sessions row, so without a per-provider dimension /start is a cheap
// way to spam rows for one provider from many-but-few IPs. Generous for real users
// (a login is a handful of taps). `validate:{ip:false}` scopes off v7's IPv6
// fallback check, which fires only because our key folds in req.ip.
const mobileSsoStartLimiter = makeLimiter({
  windowMs: 15 * 60 * 1000,
  max: 20,
  label: 'mobile-sso-start',
  message: 'Too many sign-in attempts. Please try again later.',
  keyGenerator: (req) => `${req.ip}:${req.query && req.query.provider ? req.query.provider : ''}`,
  validate: { ip: false },
})

// Mobile SSO bridge — throttle /exchange per IP. The code is single-use, PKCE-bound
// and short-lived, but cap redemption attempts anyway to blunt guessing.
const mobileSsoExchangeLimiter = makeLimiter({
  windowMs: 15 * 60 * 1000,
  max: 30,
  label: 'mobile-sso-exchange',
  message: 'Too many attempts. Please try again later.',
})

// Password-reset requests per IP. Each one can send email, so cap tighter than
// login to blunt email-bombing and enumeration timing probes. The endpoint always
// returns a generic success regardless of match, so honest users never see this.
const passwordResetRequestLimiter = makeLimiter({
  windowMs: 60 * 60 * 1000,
  max: 5,
  label: 'password-reset-request',
  message: 'Too many reset requests. Please try again later.',
})

// Reset confirmations (token + new password) per IP. A wrong/expired token is a
// guessing surface; the token itself is 256-bit random, but cap anyway.
const passwordResetConfirmLimiter = makeLimiter({
  windowMs: 15 * 60 * 1000,
  max: 15,
  label: 'password-reset-confirm',
  message: 'Too many attempts. Please try again later.',
})

// CSP violation reports. Unauthenticated by necessity (browsers send them with no
// session), and every accepted report writes a log line — so an attacker who can get
// a victim to load a page could otherwise use it as a log-flood amplifier. Generous
// enough for the real case: a genuinely broken directive fires a handful of times per
// page load, and browsers already de-duplicate identical violations per document.
const cspReportLimiter = makeLimiter({
  windowMs: 5 * 60 * 1000,
  max: 60,
  label: 'csp-report',
  message: 'Too many reports.',
})

module.exports = {
  // Exported for modules (MODULE_API.md 2.3, added in API 1.1.0). A module
  // writes its own policy -- the window and the cap are its business, since it
  // knows what its endpoints cost -- but it takes the PLUMBING from here: one
  // express-rate-limit in the process, one store, and one place limit breaches
  // are logged. A module resolving the package itself would get a second store,
  // and a limit enforced by two independent counters is not the limit either of
  // them states.
  makeLimiter,
  loginLimiter,
  registerLimiter,
  accountChangeLimiter,
  contactLimiter,
  mobileRefreshLimiter,
  ssoStartLimiter,
  mobileSsoStartLimiter,
  mobileSsoExchangeLimiter,
  passwordResetRequestLimiter,
  passwordResetConfirmLimiter,
  cspReportLimiter,
}
