import type { D1DatabaseLike } from "./d1.ts";

interface CountRow {
  count: number;
}

export interface RateLimitResult {
  allowed: boolean;
  remaining: number;
  retryAfterSeconds: number;
}

export function clientAddress(request: Request): string {
  return request.headers.get("cf-connecting-ip")?.trim() || "unknown";
}

export function rateLimitResponse(retryAfterSeconds: number): Response {
  return Response.json({
    error: {
      code: "rate_limited",
      message: "操作过于频繁，请稍后再试。",
    },
  }, {
    status: 429,
    headers: { "retry-after": String(Math.max(1, Math.ceil(retryAfterSeconds))) },
  });
}

export class RateLimitRepository {
  private operations = 0;
  private readonly database: D1DatabaseLike;
  private readonly now: () => Date;

  constructor(
    database: D1DatabaseLike,
    now: () => Date = () => new Date(),
  ) {
    this.database = database;
    this.now = now;
  }

  async consume(
    action: string,
    clientKey: string,
    policy: { limit: number; windowSeconds: number },
  ): Promise<RateLimitResult> {
    if (!/^[a-z][a-z0-9_-]{0,39}$/.test(action)) throw new Error("限速动作格式无效。");
    if (!Number.isInteger(policy.limit) || policy.limit < 1) throw new Error("限速次数无效。");
    if (!Number.isInteger(policy.windowSeconds) || policy.windowSeconds < 1) throw new Error("限速窗口无效。");
    const now = this.now();
    const nowSeconds = Math.floor(now.getTime() / 1000);
    const windowStart = Math.floor(nowSeconds / policy.windowSeconds) * policy.windowSeconds;
    const updatedAt = now.toISOString();
    await this.database.prepare(`
      INSERT INTO rate_limits (action, client_key, window_start, count, updated_at)
      VALUES (?, ?, ?, 1, ?)
      ON CONFLICT(action, client_key, window_start) DO UPDATE SET
        count = rate_limits.count + 1,
        updated_at = excluded.updated_at
    `).bind(action, clientKey.slice(0, 128), windowStart, updatedAt).run();
    const row = await this.database.prepare(`
      SELECT count FROM rate_limits
      WHERE action = ? AND client_key = ? AND window_start = ?
      LIMIT 1
    `).bind(action, clientKey.slice(0, 128), windowStart).first<CountRow>();

    this.operations += 1;
    if (this.operations % 20 === 0 || this.operations === 3) {
      const cutoff = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
      await this.database.prepare("DELETE FROM rate_limits WHERE updated_at < ?").bind(cutoff).run();
    }

    const count = Number(row?.count ?? 1);
    const allowed = count <= policy.limit;
    return {
      allowed,
      remaining: Math.max(0, policy.limit - count),
      retryAfterSeconds: allowed ? 0 : Math.max(1, windowStart + policy.windowSeconds - nowSeconds),
    };
  }
}
