import type {
  IRateLimit,
  RateLimitBudget,
  RateLimitDecision,
} from "./rate-limit.interface";

const UNIT_MS: Record<string, number> = {
  ms: 1,
  s: 1_000,
  m: 60_000,
  h: 3_600_000,
  d: 86_400_000,
};

function parseWindowMs(window: string): number {
  const match = /^(\d+)(ms|s|m|h|d)$/.exec(window);
  if (!match) throw new Error(`Invalid rate-limit window: "${window}"`);
  return parseInt(match[1] as string, 10) * UNIT_MS[match[2] as string]!;
}

type Bucket = { count: number; resetAt: number };

export class InMemoryRateLimit implements IRateLimit {
  private readonly budgets: Map<string, RateLimitBudget>;
  private readonly buckets = new Map<string, Bucket>();
  private readonly clock: () => number;

  constructor(
    budgets: RateLimitBudget[],
    clock: () => number = () => Date.now(),
  ) {
    this.budgets = new Map(budgets.map((b) => [b.name, b]));
    this.clock = clock;
  }

  async consume(
    budgetName: string,
    key: string,
    weight = 1,
  ): Promise<RateLimitDecision> {
    const budget = this.budgets.get(budgetName);
    if (!budget) throw new Error(`Unknown rate-limit budget: "${budgetName}"`);

    const windowMs = parseWindowMs(budget.window);
    const now = this.clock();
    const bucketKey = `${budgetName}:${key}`;

    let bucket = this.buckets.get(bucketKey);
    if (!bucket || now >= bucket.resetAt) {
      bucket = { count: 0, resetAt: now + windowMs };
      this.buckets.set(bucketKey, bucket);
    }

    const resetAt = new Date(bucket.resetAt);

    if (bucket.count + weight > budget.budget) {
      return { allowed: false, remaining: 0, resetAt };
    }

    bucket.count += weight;
    return { allowed: true, remaining: budget.budget - bucket.count, resetAt };
  }

  async reset(budgetName: string, key: string): Promise<void> {
    this.buckets.delete(`${budgetName}:${key}`);
  }
}
