const tenMinutesMs = 10 * 60 * 1000;

export const defaultAuthenticationRateLimits = Object.freeze({
  windowMs: tenMinutesMs,
  blockMs: tenMinutesMs,
  combinationLimit: 10,
  accountLimit: 30,
  ipLimit: 100,
  maxEntries: 50_000,
  cleanupIntervalMs: 60 * 1000
});

export class AuthenticationRateLimiter {
  constructor(options = {}) {
    this.windowMs = positiveInteger(options.windowMs, defaultAuthenticationRateLimits.windowMs);
    this.blockMs = positiveInteger(options.blockMs, defaultAuthenticationRateLimits.blockMs);
    this.combinationLimit = positiveInteger(
      options.combinationLimit,
      defaultAuthenticationRateLimits.combinationLimit
    );
    this.accountLimit = positiveInteger(options.accountLimit, defaultAuthenticationRateLimits.accountLimit);
    this.ipLimit = positiveInteger(options.ipLimit, defaultAuthenticationRateLimits.ipLimit);
    this.maxEntries = positiveInteger(options.maxEntries, defaultAuthenticationRateLimits.maxEntries);
    this.cleanupIntervalMs = positiveInteger(
      options.cleanupIntervalMs,
      defaultAuthenticationRateLimits.cleanupIntervalMs
    );
    this.now = typeof options.now === 'function' ? options.now : Date.now;
    this.entries = new Map();
    this.lastCleanupAt = 0;
  }

  isBlocked({ ip, account }) {
    const now = this.now();
    this.cleanupIfNeeded(now);
    return authenticationKeys(ip, account).some(({ key }) => {
      const entry = this.readEntry(key, now);
      return Boolean(entry && entry.blockedUntil > now);
    });
  }

  recordFailure({ ip, account }) {
    const now = this.now();
    this.cleanupIfNeeded(now);
    let blocked = false;
    for (const { key, limit } of authenticationKeys(ip, account, this)) {
      const entry = this.readEntry(key, now) || { failures: [], blockedUntil: 0 };
      entry.failures.push(now);
      if (entry.failures.length >= limit) {
        entry.blockedUntil = Math.max(entry.blockedUntil, now + this.blockMs);
        blocked = true;
      }
      this.touchEntry(key, entry);
    }
    this.enforceEntryLimit();
    return blocked;
  }

  recordSuccess({ ip, account }) {
    const normalizedIp = normalizeAuthenticationIp(ip);
    const normalizedAccount = normalizeAuthenticationAccount(account);
    this.entries.delete(combinationKey(normalizedIp, normalizedAccount));
    this.entries.delete(accountKey(normalizedAccount));
  }

  reset() {
    this.entries.clear();
    this.lastCleanupAt = 0;
  }

  get entryCount() {
    return this.entries.size;
  }

  readEntry(key, now) {
    const entry = this.entries.get(key);
    if (!entry) return null;
    const cutoff = now - this.windowMs;
    entry.failures = entry.failures.filter((timestamp) => timestamp > cutoff);
    if (entry.blockedUntil <= now) entry.blockedUntil = 0;
    if (!entry.failures.length && !entry.blockedUntil) {
      this.entries.delete(key);
      return null;
    }
    this.touchEntry(key, entry);
    return entry;
  }

  touchEntry(key, entry) {
    this.entries.delete(key);
    this.entries.set(key, entry);
  }

  cleanupIfNeeded(now) {
    if (
      this.entries.size <= this.maxEntries
      && now - this.lastCleanupAt < this.cleanupIntervalMs
    ) return;
    for (const key of [...this.entries.keys()]) this.readEntry(key, now);
    this.enforceEntryLimit();
    this.lastCleanupAt = now;
  }

  enforceEntryLimit() {
    while (this.entries.size > this.maxEntries) {
      const oldestKey = this.entries.keys().next().value;
      if (oldestKey === undefined) return;
      this.entries.delete(oldestKey);
    }
  }
}

export const authenticationRateLimiter = new AuthenticationRateLimiter();

export function authenticateWithRateLimit({ limiter = authenticationRateLimiter, ip, account, authenticate }) {
  const identity = { ip, account };
  if (limiter.isBlocked(identity)) return null;
  const result = authenticate();
  if (result) {
    limiter.recordSuccess(identity);
    return result;
  }
  limiter.recordFailure(identity);
  return null;
}

export async function authenticateWithRateLimitAsync({
  limiter = authenticationRateLimiter,
  ip,
  account,
  authenticate
}) {
  const identity = { ip, account };
  if (limiter.isBlocked(identity)) return null;
  const result = await authenticate();
  if (result) {
    limiter.recordSuccess(identity);
    return result;
  }
  limiter.recordFailure(identity);
  return null;
}

export function normalizeAuthenticationIp(value) {
  const raw = String(value || '').trim().toLowerCase();
  if (!raw) return 'unknown';
  const unwrapped = raw.startsWith('[') && raw.endsWith(']') ? raw.slice(1, -1) : raw;
  return (unwrapped.startsWith('::ffff:') ? unwrapped.slice(7) : unwrapped).slice(0, 128);
}

export function normalizeAuthenticationAccount(value) {
  return String(value || '').trim().toLowerCase().slice(0, 320) || 'unknown';
}

function authenticationKeys(ip, account, limiter) {
  const normalizedIp = normalizeAuthenticationIp(ip);
  const normalizedAccount = normalizeAuthenticationAccount(account);
  return [
    {
      key: combinationKey(normalizedIp, normalizedAccount),
      limit: limiter?.combinationLimit
    },
    {
      key: accountKey(normalizedAccount),
      limit: limiter?.accountLimit
    },
    {
      key: ipKey(normalizedIp),
      limit: limiter?.ipLimit
    }
  ];
}

function combinationKey(ip, account) {
  return `combination:${JSON.stringify([ip, account])}`;
}

function accountKey(account) {
  return `account:${account}`;
}

function ipKey(ip) {
  return `ip:${ip}`;
}

function positiveInteger(value, fallback) {
  const parsed = Number(value);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
