export declare class RateLimiter {
  private windowMs: number
  private maxRequests: number
  private storage: StorageProvider
  private keyGenerator: (request: Request) => string | Promise<string>
  private skipFailedRequests: boolean
  private algorithm: RateLimitAlgorithm
  private standardHeaders: boolean
  private legacyHeaders: boolean
  private skipFn?: (request: Request) => boolean | Promise<boolean>
  private handler?: (request: Request, result: RateLimitResult) => Response | Promise<Response>
  private draftMode: boolean
  private tokenBucketOptions?: TokenBucketOptions
  private tokenBuckets: Map<string, { tokens: number, lastRefill: number }>

  constructor(options: RateLimiterOptions) {
    this.windowMs = options.windowMs || config.windowMs || 60 * 1000
    this.maxRequests = options.maxRequests || config.maxRequests || 100

    if (options.maxRequests === 0) {
      this.maxRequests = 0
    }

    if (options.storage) {
      this.storage = options.storage
    }
    else if (config.storage === 'redis' && config.redis) {
      throw new Error('Redis client must be provided explicitly when using Redis storage')
    }
    else {
      this.storage = new MemoryStorage(config.memoryStorage)
    }

    this.skipFailedRequests = options.skipFailedRequests ?? false
    this.keyGenerator = options.keyGenerator || this.defaultKeyGenerator
    this.algorithm = options.algorithm || config.algorithm || 'fixed-window'
    this.standardHeaders = options.standardHeaders ?? config.standardHeaders ?? true
    this.legacyHeaders = options.legacyHeaders ?? config.legacyHeaders ?? true
    this.skipFn = options.skip
    this.handler = options.handler
    this.draftMode = options.draftMode ?? config.draftMode ?? false
    this.tokenBuckets = new Map()

    if (this.algorithm === 'token-bucket') {
      this.tokenBucketOptions = {
        capacity: this.maxRequests,
        refillRate: this.maxRequests / (this.windowMs / 1000) / 1000, 
      }
    }

    if (config.verbose) {
      const storageType = options.storage
        ? (options.storage instanceof MemoryStorage ? 'memory (custom)' : 'redis (custom)')
        : config.storage

      console.warn(`[ts-rate-limiter] Initialized with:
  - Algorithm: ${this.algorithm}
  - Window: ${this.windowMs}ms
  - Max Requests: ${this.maxRequests}
  - Storage: ${storageType}
  - Draft Mode: ${this.draftMode ? 'enabled' : 'disabled'}`)
    }
  }

  private defaultKeyGenerator(request: Request): string {
    const forwarded = request.headers.get('x-forwarded-for')
    if (forwarded) {
      return forwarded.split(',')[0].trim()
    }

    const clientIP = request.headers.get('x-client-ip')
    if (clientIP) {
      return clientIP
    }

    const realIP = request.headers.get('x-real-ip')
    if (realIP) {
      return realIP
    }

    const socket = (request as any).socket
    return socket?.remoteAddress || '127.0.0.1'
  }

  async check(request: Request): Promise<RateLimitResult> {
    try {
      if (this.skipFn && await this.skipFn(request)) {
        return this.createAllowedResult()
      }

      if (this.maxRequests === 0) {
        return {
          allowed: false,
          current: 1,
          limit: 0,
          remaining: this.windowMs,
          resetTime: Date.now() + this.windowMs,
        }
      }

      const key = await this.keyGenerator(request)

      if (this.algorithm === 'sliding-window' && this.storage.getSlidingWindowCount) {
        return this.checkSlidingWindow(key)
      }
      else if (this.algorithm === 'token-bucket') {
        return this.checkTokenBucket(key)
      }
      else {
        return this.checkFixedWindow(key)
      }
    }
    catch (error) {
      if (this.skipFailedRequests) {
        return this.createAllowedResult()
      }
      throw error
    }
  }

  private async checkFixedWindow(key: string): Promise<RateLimitResult> {
    const { count, resetTime } = await this.storage.increment(key, this.windowMs)
    const remaining = Math.max(0, resetTime - Date.now())
    const allowed = this.draftMode ? true : count <= this.maxRequests

    return {
      allowed,
      current: count,
      limit: this.maxRequests,
      remaining,
      resetTime,
    }
  }

  private async checkSlidingWindow(key: string): Promise<RateLimitResult> {
    if (!this.storage.getSlidingWindowCount) {
      return this.checkFixedWindow(key)
    }

    await this.storage.increment(key, this.windowMs)
    const count = await this.storage.getSlidingWindowCount(key, this.windowMs)
    const now = Date.now()
    const resetTime = now + this.windowMs
    const allowed = this.draftMode ? true : count <= this.maxRequests

    return {
      allowed,
      current: count,
      limit: this.maxRequests,
      remaining: this.windowMs,
      resetTime,
    }
  }

  private async checkTokenBucket(key: string): Promise<RateLimitResult> {
    if (!this.tokenBucketOptions) {
      return this.checkFixedWindow(key)
    }

    const now = Date.now()
    let bucket = this.tokenBuckets.get(key)

    if (!bucket) {
      bucket = {
        tokens: this.tokenBucketOptions.capacity,
        lastRefill: now,
      }
      this.tokenBuckets.set(key, bucket)
    }

    const timeElapsed = now - bucket.lastRefill
    const tokensToAdd = timeElapsed * this.tokenBucketOptions.refillRate

    if (tokensToAdd > 0) {
      bucket.tokens = Math.min(
        bucket.tokens + tokensToAdd,
        this.tokenBucketOptions.capacity,
      )
      bucket.lastRefill = now
    }

    const allowed = this.draftMode ? true : bucket.tokens >= 1

    if (allowed && !this.draftMode) {
      bucket.tokens -= 1
    }

    const msUntilNextToken = bucket.tokens >= this.tokenBucketOptions.capacity
      ? this.windowMs
      : Math.ceil(1 / this.tokenBucketOptions.refillRate)

    return {
      allowed,
      current: this.tokenBucketOptions.capacity - Math.floor(bucket.tokens),
      limit: this.tokenBucketOptions.capacity,
      remaining: msUntilNextToken,
      resetTime: now + msUntilNextToken,
    }
  }

  private createAllowedResult(): RateLimitResult {
    return {
      allowed: true,
      current: 0,
      limit: this.maxRequests,
      remaining: this.windowMs,
      resetTime: Date.now() + this.windowMs,
    }
  }

  async consume(key: string): Promise<RateLimitResult> {
    if (this.algorithm === 'sliding-window' && this.storage.getSlidingWindowCount) {
      return this.checkSlidingWindow(key)
    }
    else if (this.algorithm === 'token-bucket') {
      return this.checkTokenBucket(key)
    }
    else {
      return this.checkFixedWindow(key)
    }
  }

  async reset(key: string): Promise<void> {
    await this.storage.reset(key)

    if (this.algorithm === 'token-bucket') {
      this.tokenBuckets.delete(key)
    }
  }

  async resetAll(): Promise<void> {
    if (this.algorithm === 'token-bucket') {
      this.tokenBuckets.clear()
    }

    if (this.storage.cleanExpired) {
      this.storage.cleanExpired()
    }
  }

  private getHeaders(result: RateLimitResult): Record<string, string> {
    const headers: Record<string, string> = {}

    if (this.standardHeaders) {
      headers['RateLimit-Limit'] = this.maxRequests.toString()
      headers['RateLimit-Remaining'] = Math.max(0, this.maxRequests - result.current).toString()
      headers['RateLimit-Reset'] = Math.ceil(result.resetTime / 1000).toString()
    }

    if (this.legacyHeaders) {
      headers['X-RateLimit-Limit'] = this.maxRequests.toString()
      headers['X-RateLimit-Remaining'] = Math.max(0, this.maxRequests - result.current).toString()
      headers['X-RateLimit-Reset'] = Math.ceil(result.resetTime / 1000).toString()

      if (!result.allowed) {
        headers['Retry-After'] = Math.ceil(result.remaining / 1000).toString()
      }
    }

    return headers
  }

  middleware(): (req: Request) => Promise<Response | null> {
    return async (req: Request) => {
      const result = await this.check(req)

      if (!result.allowed) {
        if (this.handler) {
          return this.handler(req, result)
        }

        return new Response('Rate limit exceeded', {
          status: 429,
          headers: this.getHeaders(result),
        })
      }


      return null
    }
  }

  dispose(): void {
    if (this.storage.dispose) {
      this.storage.dispose()
    }

    this.tokenBuckets.clear()
  }
}