import type {
  PasskeyAuthenticationOptions,
  PasskeyProvider,
  PasskeyRegistrationOptions,
} from "../types.ts";

export type { PasskeyProvider } from "../types.ts";

export type PasskeyChallengeKind = "registration" | "authentication";

export interface PasskeyChallengeRecord {
  challenge: string;
  kind: PasskeyChallengeKind;
  userId?: string;
  rpId: string;
  origin: string;
  expiresAt: number;
}

/**
 * Durable challenge storage contract. Production deployments with more than one
 * process should provide a shared implementation (for example Redis or SQL).
 */
export interface PasskeyChallengeStore {
  set(key: string, value: PasskeyChallengeRecord): Promise<void>;
  consume(key: string): Promise<PasskeyChallengeRecord | undefined>;
}

export class MemoryPasskeyChallengeStore implements PasskeyChallengeStore {
  private readonly values = new Map<string, PasskeyChallengeRecord>();
  private readonly now: () => number;

  constructor(now: () => number = () => Date.now()) {
    this.now = now;
  }

  private pruneExpired(): void {
    const timestamp = this.now();
    for (const [key, value] of this.values) {
      if (value.expiresAt <= timestamp) this.values.delete(key);
    }
  }

  async set(key: string, value: PasskeyChallengeRecord): Promise<void> {
    this.pruneExpired();
    this.values.set(key, { ...value });
  }

  async consume(key: string): Promise<PasskeyChallengeRecord | undefined> {
    this.pruneExpired();
    const value = this.values.get(key);
    this.values.delete(key);
    return value ? { ...value } : undefined;
  }
}

export function assertPasskeyProvider(provider: PasskeyProvider | undefined): PasskeyProvider {
  if (!provider) throw new Error("WRN-AUTH-PASSKEY-PROVIDER: configure a PasskeyProvider");
  return provider;
}

export function publicKeyCreationOptions(
  options: PasskeyRegistrationOptions,
): PublicKeyCredentialCreationOptions {
  return {
    ...options,
    challenge: decode(options.challenge),
    user: { ...options.user, id: decode(options.user.id) },
    excludeCredentials: options.excludeCredentials?.map((item) => ({
      ...item,
      id: decode(item.id),
      transports: item.transports as AuthenticatorTransport[] | undefined,
    })),
  } as unknown as PublicKeyCredentialCreationOptions;
}

export function publicKeyRequestOptions(
  options: PasskeyAuthenticationOptions,
): PublicKeyCredentialRequestOptions {
  return {
    ...options,
    challenge: decode(options.challenge),
    allowCredentials: options.allowCredentials?.map((item) => ({
      ...item,
      id: decode(item.id),
      transports: item.transports as AuthenticatorTransport[] | undefined,
    })),
  } as unknown as PublicKeyCredentialRequestOptions;
}

function decode(value: string): Uint8Array {
  if (!value || !/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) {
    throw new TypeError("Passkey data must be canonical Base64URL without padding");
  }
  const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
  const binary = atob(normalized + "=".repeat((4 - (normalized.length % 4)) % 4));
  const bytes = new Uint8Array(binary.length);
  for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
  return bytes;
}
