import { createHash } from 'node:crypto';
import { oAuthSafeStringCompare } from '../../../shared/functions/o-auth-safe-string-compare.function';
import { InvalidRequestError } from '../../domain/errors';

interface VerifyPkceS256Args {
  codeVerifier: string;
  codeChallenge: string;
  /** Only 'S256' is accepted; anything else (including 'plain' and undefined) throws invalid_request. */
  codeChallengeMethod: string | undefined;
}

/**
 * Verifies the PKCE S256 exchange as required by RFC 7636 and OAuth 2.1 (decision D3).
 *
 * Algorithm:
 *   expected = BASE64URL(SHA256(code_verifier))
 * If expected === code_challenge the function returns normally.
 * Any other outcome throws InvalidRequestError.
 *
 * ONLY `S256` is supported. `plain` and any other method — including missing/undefined —
 * are rejected immediately with `invalid_request` (D3: "Defense-in-depth even for
 * confidential first-party clients").
 *
 * Empty strings for either `codeVerifier` or `codeChallenge` are treated as a mismatch
 * and also produce `invalid_request`.
 *
 * The comparison uses `oAuthSafeStringCompare` (constant-time) for timing hygiene,
 * consistent with secret comparison elsewhere in the OAuth flows.
 *
 * At the AUTHORIZE endpoint this function throws `InvalidRequestError` on any failure.
 * At the TOKEN EXCHANGE endpoint callers should catch `InvalidRequestError` from this
 * helper and rethrow as `InvalidGrantError` per RFC 7636 §4.6.
 */
export const oAuthVerifyPkceS256 = (args: VerifyPkceS256Args): void => {
  // 1. Reject every method except S256.
  if (args.codeChallengeMethod !== 'S256') {
    throw new InvalidRequestError();
  }

  // 2. Reject trivially empty inputs before hashing.
  if (!args.codeVerifier || !args.codeChallenge) {
    throw new InvalidRequestError();
  }

  // 3. Compute BASE64URL(SHA256(code_verifier)) and constant-time compare.
  const expectedChallenge = createHash('sha256').update(args.codeVerifier).digest('base64url');

  if (!oAuthSafeStringCompare(expectedChallenge, args.codeChallenge)) {
    throw new InvalidRequestError();
  }
};
