import type { CaptchaChallengeRecord, CaptchaResponseTokenRecord, CaptchaStore } from "../types.ts";

function cloneChallenge(record: CaptchaChallengeRecord): CaptchaChallengeRecord {
  return structuredClone(record);
}

function cloneToken(record: CaptchaResponseTokenRecord): CaptchaResponseTokenRecord {
  return structuredClone(record);
}

export interface MemoryCaptchaStoreOptions {
  maxChallenges?: number;
  maxTokens?: number;
}

export class MemoryCaptchaStore implements CaptchaStore {
  private readonly challenges = new Map<string, CaptchaChallengeRecord>();
  private readonly tokens = new Map<string, CaptchaResponseTokenRecord>();
  private readonly maxChallenges: number;
  private readonly maxTokens: number;

  constructor(options: MemoryCaptchaStoreOptions = {}) {
    this.maxChallenges = options.maxChallenges ?? 25_000;
    this.maxTokens = options.maxTokens ?? 50_000;
    if (!Number.isInteger(this.maxChallenges) || this.maxChallenges < 1) {
      throw new RangeError("maxChallenges must be a positive integer");
    }
    if (!Number.isInteger(this.maxTokens) || this.maxTokens < 1) {
      throw new RangeError("maxTokens must be a positive integer");
    }
  }

  async createChallenge(record: CaptchaChallengeRecord): Promise<void> {
    this.evict(this.challenges, this.maxChallenges, record.createdAt);
    this.challenges.set(record.id, cloneChallenge(record));
  }

  async getChallenge(id: string): Promise<CaptchaChallengeRecord | undefined> {
    const record = this.challenges.get(id);
    return record ? cloneChallenge(record) : undefined;
  }

  async incrementAttempts(id: string, now: number): Promise<CaptchaChallengeRecord | undefined> {
    const record = this.challenges.get(id);
    if (!record || record.expiresAt <= now || record.consumedAt) return undefined;
    record.attempts += 1;
    return cloneChallenge(record);
  }

  async consumeChallenge(id: string, now: number): Promise<CaptchaChallengeRecord | undefined> {
    const record = this.challenges.get(id);
    if (!record || record.expiresAt <= now || record.consumedAt) return undefined;
    record.consumedAt = now;
    return cloneChallenge(record);
  }

  async deleteChallenge(id: string): Promise<void> {
    this.challenges.delete(id);
  }

  async createToken(record: CaptchaResponseTokenRecord): Promise<void> {
    this.evict(this.tokens, this.maxTokens, record.createdAt);
    this.tokens.set(record.tokenHash, cloneToken(record));
  }

  async getToken(tokenHash: string): Promise<CaptchaResponseTokenRecord | undefined> {
    const record = this.tokens.get(tokenHash);
    return record ? cloneToken(record) : undefined;
  }

  async consumeToken(
    tokenHash: string,
    now: number,
  ): Promise<CaptchaResponseTokenRecord | undefined> {
    const record = this.tokens.get(tokenHash);
    if (!record || record.expiresAt <= now || record.consumedAt) return undefined;
    record.consumedAt = now;
    return cloneToken(record);
  }

  async deleteToken(tokenHash: string): Promise<void> {
    this.tokens.delete(tokenHash);
  }

  async gc(now: number): Promise<void> {
    for (const [id, record] of this.challenges) {
      if (record.expiresAt <= now || (record.consumedAt && record.consumedAt + 60_000 <= now)) {
        this.challenges.delete(id);
      }
    }
    for (const [hash, record] of this.tokens) {
      if (record.expiresAt <= now || (record.consumedAt && record.consumedAt + 60_000 <= now)) {
        this.tokens.delete(hash);
      }
    }
  }

  private evict<T extends { expiresAt: number }>(
    map: Map<string, T>,
    max: number,
    now: number,
  ): void {
    for (const [key, record] of map) if (record.expiresAt <= now) map.delete(key);
    while (map.size >= max) map.delete(map.keys().next().value!);
  }
}

export function createMemoryCaptchaStore(options?: MemoryCaptchaStoreOptions): MemoryCaptchaStore {
  return new MemoryCaptchaStore(options);
}
