import { Injectable } from '@nestjs/common';
import { LoggerService, RedisService } from '@modules/shared';

export interface SmsRateLimitOptions {
  limit: number;
  windowSeconds: number;
}

export interface SmsRateLimitDecision {
  allowed: boolean;
  current: number;
  limit: number;
  retryAfterSeconds: number;
  bucket: string;
}

export const SMS_RATE_LIMIT_BUCKETS = {
  otp: { limit: 5, windowSeconds: 60 } satisfies SmsRateLimitOptions,
  otpHourly: { limit: 10, windowSeconds: 60 * 60 } satisfies SmsRateLimitOptions,
  transactional: { limit: 20, windowSeconds: 60 } satisfies SmsRateLimitOptions,
  transactionalHourly: { limit: 100, windowSeconds: 60 * 60 } satisfies SmsRateLimitOptions,
} as const;

export type SmsRateLimitBucket = keyof typeof SMS_RATE_LIMIT_BUCKETS;

const SLIDING_WINDOW_LUA = `
local key = KEYS[1]
local now = tonumber(ARGV[1])
local windowMs = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local requestId = ARGV[4]
local windowSec = tonumber(ARGV[5])

redis.call('ZREMRANGEBYSCORE', key, 0, now - windowMs)
local current = redis.call('ZCARD', key)

if current < limit then
  redis.call('ZADD', key, now, requestId)
  redis.call('EXPIRE', key, windowSec + 1)
  return {current + 1, 0}
else
  local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
  local retryAfterMs = 0
  if #oldest >= 2 then
    retryAfterMs = tonumber(oldest[2]) + windowMs - now
    if retryAfterMs < 0 then retryAfterMs = 0 end
  end
  return {current, retryAfterMs}
end
`;

let requestCounter = 0;

@Injectable()
export class SmsRateLimiterService {
  constructor(
    private readonly redis: RedisService,
    private readonly logger: LoggerService,
  ) {}

  async check(phone: string, bucket: SmsRateLimitBucket): Promise<SmsRateLimitDecision> {
    const options = SMS_RATE_LIMIT_BUCKETS[bucket];
    const key = `sms_rate_limit:${bucket}:${phone}`;

    try {
      const client = this.redis.getClient();
      const now = Date.now();
      const requestId = `${now}:${process.pid}:${++requestCounter}`;

      const result = (await client.eval(
        SLIDING_WINDOW_LUA,
        1,
        key,
        now,
        options.windowSeconds * 1000,
        options.limit,
        requestId,
        options.windowSeconds,
      )) as [number, number];

      const current = result[0];
      const retryAfterMs = result[1];
      const allowed = retryAfterMs === 0 && current <= options.limit;
      const retryAfterSeconds = allowed ? 0 : Math.max(1, Math.ceil(retryAfterMs / 1000));

      if (!allowed) {
        this.logger.warn(
          `SMS rate limit hit for ${this.maskPhone(phone)} bucket=${bucket} ` +
            `current=${current}/${options.limit} retryAfter=${retryAfterSeconds}s`,
          'SmsRateLimiterService',
        );
      }

      return {
        allowed,
        current,
        limit: options.limit,
        retryAfterSeconds,
        bucket,
      };
    } catch (error) {
      this.logger.warn(
        `SMS rate limit check failed (Redis error), failing open for ${this.maskPhone(phone)}: ` +
          `${error instanceof Error ? error.message : 'unknown'}`,
        'SmsRateLimiterService',
      );
      return {
        allowed: true,
        current: 0,
        limit: options.limit,
        retryAfterSeconds: 0,
        bucket,
      };
    }
  }

  private maskPhone(phone: string): string {
    if (phone.length <= 4) return '***';
    return `${phone.slice(0, 3)}***${phone.slice(-2)}`;
  }
}
