import type {Context} from 'hono'
import {rateLimiter, RedisStore} from 'hono-rate-limiter'

import type {GuardedVars} from '../auth/middleware'
import {env} from '../env'
import {sha256} from './hash'
import {redis} from './redis'

// node-redis v4 → hono-rate-limiter RedisClient shape
const rlClient = {
  scriptLoad: (script: string) => redis.scriptLoad(script),
  evalsha: <TArgs extends unknown[], TData = unknown>(
    sha1: string,
    keys: string[],
    args: TArgs,
  ) =>
    redis.evalSha(sha1, {
      keys,
      arguments: (args as unknown[]).map(String),
    }) as Promise<TData>,
  decr: (key: string) => redis.decr(key),
  del: (key: string) => redis.del(key),
}

// when behind a trusted proxy the last xff hop is the address the
// proxy saw; earlier hops are client-controlled and spoofable
export const resolveClientIp = ({
  xff,
  socketIp,
  trustProxy,
}: {
  xff: string | undefined
  socketIp: string | null | undefined
  trustProxy: boolean
}): string | null => {
  if (trustProxy) {
    const last = xff
      ?.split(',')
      .map((hop) => hop.trim())
      .filter(Boolean)
      .at(-1)
    if (last) return last
  }
  return socketIp ?? null
}

const ipKey = (c: Context) =>
  resolveClientIp({
    xff: c.req.header('x-forwarded-for'),
    socketIp: (c.env as {ip?: string | null} | undefined)?.ip,
    trustProxy: env.TRUST_PROXY,
  }) ?? 'unknown'

type LimiterOpts = {
  prefix: string
  windowMs: number
  limit: number
  keyGenerator?: (c: Context) => string
}

const makeLimiter = ({
  prefix,
  windowMs,
  limit,
  keyGenerator,
}: LimiterOpts) =>
  rateLimiter({
    windowMs,
    limit,
    standardHeaders: 'draft-7',
    keyGenerator: keyGenerator ?? ipKey,
    store: new RedisStore({client: rlClient, prefix}),
  })

const EMAIL_QUOTA_LIMIT = 3
const EMAIL_QUOTA_WINDOW_SECONDS = 900

// per-email cap is immune to ip spoofing; called with the validated,
// normalized email so it can't run as middleware
export const consumeEmailQuota = async (
  email: string,
): Promise<boolean> => {
  const key = `rl:email:${sha256(email)}`
  const count = await redis.incr(key)
  await redis.expire(key, EMAIL_QUOTA_WINDOW_SECONDS, 'NX')
  return count <= EMAIL_QUOTA_LIMIT
}

export const authRequestLimiter = makeLimiter({
  prefix: 'rl:auth:',
  windowMs: 60_000,
  limit: 5,
})

export const authVerifyLimiter = makeLimiter({
  prefix: 'rl:verify:',
  windowMs: 60_000,
  limit: 10,
})

export const authExchangeLimiter = makeLimiter({
  prefix: 'rl:exchange:',
  windowMs: 60_000,
  limit: 10,
})

// per-user quotas for routes that proxy paid upstream apis
const makeUserLimiter = ({
  prefix,
  windowMs,
  limit,
}: Omit<LimiterOpts, 'keyGenerator'>) =>
  rateLimiter<{Variables: GuardedVars}>({
    windowMs,
    limit,
    standardHeaders: 'draft-7',
    keyGenerator: (c) => c.get('user').id,
    store: new RedisStore({client: rlClient, prefix}),
  })

export const placesLimiter = makeUserLimiter({
  prefix: 'rl:places:',
  windowMs: 60_000,
  limit: 60,
})

export const extractLimiter = makeUserLimiter({
  prefix: 'rl:extract:',
  windowMs: 60_000,
  limit: 10,
})

export const avatarLimiter = makeUserLimiter({
  prefix: 'rl:avatar:',
  windowMs: 60_000,
  limit: 10,
})

export const crumbImageLimiter = makeUserLimiter({
  prefix: 'rl:crumb-image:',
  windowMs: 60_000,
  limit: 20,
})

export const trailCoverLimiter = makeUserLimiter({
  prefix: 'rl:trail-cover:',
  windowMs: 60_000,
  limit: 20,
})
