/**
 * OAuth 2.0 Authorization Code flow — pure crypto helpers (wave-12).
 *
 * All crypto ops use Web Crypto (crypto.subtle). No `import from 'crypto'`.
 * Token comparison uses timingSafeEqual (no-string-equality-for-tokens rule).
 *
 * DB-aware functions (issueAuthorizationCode, exchangeAuthorizationCode, etc.)
 * live in packages/db/src/queries/oauth.ts (same pattern as auth-writes.ts).
 */
import { generateOpaqueToken } from './tokens'
import { timingSafeEqual } from './crypto'

// ── Constants ─────────────────────────────────────────────────────────────────

export const ACCESS_TOKEN_PREFIX  = 'zyk_live_'
export const REFRESH_TOKEN_PREFIX = 'zyk_rt_'

export const ACCESS_TTL_SECONDS   = 60 * 60          // 1 hour
export const REFRESH_TTL_SECONDS  = 60 * 24 * 60 * 60 // 60 days
export const CODE_TTL_SECONDS     = 10 * 60            // 10 minutes
export const CONSENT_TTL_SECONDS  = 15 * 60            // 15 minutes

// ── PKCE ──────────────────────────────────────────────────────────────────────

/**
 * Verify PKCE S256 challenge.
 * challenge = base64url(SHA-256(verifier)) — URL-safe base64, no padding.
 */
export async function verifyPKCE(
  codeVerifier: string,
  storedChallenge: string,
): Promise<boolean> {
  const encoder = new TextEncoder()
  const data = encoder.encode(codeVerifier)
  const digest = await crypto.subtle.digest('SHA-256', data)
  const computed = base64urlEncode(digest)
  return timingSafeEqual(computed, storedChallenge)
}

/** Base64url encode (no padding, URL-safe alphabet). */
export function base64urlEncode(buffer: ArrayBuffer): string {
  const bytes = new Uint8Array(buffer)
  let binary = ''
  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i]!)
  }
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
}

// ── Consent CSRF token ────────────────────────────────────────────────────────

/**
 * Issue a per-session consent anti-CSRF token.
 * Binds to (sessionSub, clientId) via HMAC-SHA256 keyed with JWT_SECRET.
 * Format: `<nonce>.<hmac-base64url>`
 */
export async function issueConsentToken(
  jwtSecret: string,
  sessionSub: string,
  clientId: string,
): Promise<string> {
  const nonce = generateOpaqueToken()
  const message = `${nonce}:${sessionSub}:${clientId}`
  const encoder = new TextEncoder()
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(jwtSecret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  )
  const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(message))
  const sigB64 = base64urlEncode(sig)
  return `${nonce}.${sigB64}`
}

/**
 * Verify a consent anti-CSRF token.
 * Returns true iff the token was issued for (sessionSub, clientId).
 */
export async function verifyConsentToken(
  jwtSecret: string,
  token: string,
  sessionSub: string,
  clientId: string,
): Promise<boolean> {
  const dotIdx = token.lastIndexOf('.')
  if (dotIdx < 1) return false
  const nonce = token.slice(0, dotIdx)
  const providedSig = token.slice(dotIdx + 1)
  const message = `${nonce}:${sessionSub}:${clientId}`
  const encoder = new TextEncoder()
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(jwtSecret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  )
  const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(message))
  const expectedSig = base64urlEncode(sig)
  return timingSafeEqual(providedSig, expectedSig)
}

// ── Scope validation ──────────────────────────────────────────────────────────

/** Normalise a scope string to a deduplicated sorted array. */
export function normalizeRequestedScope(scope: string): string[] {
  return [...new Set(scope.trim().split(/\s+/).filter(Boolean))].sort()
}

/** Assert every requested scope is in client's allowed list. Throws OAuthError on mismatch. */
export function assertScopesAllowed(requested: string[], clientScopes: string[]): void {
  const allowed = new Set(clientScopes)
  const invalid = requested.filter((s) => !allowed.has(s))
  if (invalid.length > 0) {
    throw new OAuthError('invalid_scope', `Scope not allowed: ${invalid.join(' ')}`)
  }
}

/** Validate redirect_uri — exact string match. Throws OAuthError on mismatch. */
export function validateRedirectUri(uri: string, registeredUris: string[]): void {
  if (!registeredUris.includes(uri)) {
    throw new OAuthError('invalid_request', 'redirect_uri mismatch')
  }
}

// ── OAuth error ───────────────────────────────────────────────────────────────

export class OAuthError extends Error {
  constructor(
    public readonly code: string,
    message?: string,
  ) {
    super(message ?? code)
    this.name = 'OAuthError'
  }
}
