/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.core.auth.sso

import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64

/**
 * PKCE + CSRF-state primitives for the Mobile SSO Authorization Bridge — "Layer B"
 * of the two PKCE layers (app ↔ website; PLAN.md §4.2, BACKEND_DESIGN "Two PKCE
 * layers"). The app proves at `/exchange` that it holds the verifier for the
 * challenge it registered at `/start`, so an intercepted callback code is useless
 * to anyone but this app.
 *
 * Pure JVM (no Android framework types) so it unit-tests on the plain test runner.
 * The encoding mirrors the backend exactly (RFC 7636 S256): the challenge is
 * `base64url(SHA-256(verifier))` with no padding, matching Node's
 * `crypto.createHash('sha256').update(verifier).digest('base64url')`.
 */
object Pkce {

    private val random = SecureRandom()

    // RFC 4648 §5 URL-safe base64 without padding — the base64url the backend uses.
    private val encoder = Base64.getUrlEncoder().withoutPadding()

    /**
     * A fresh high-entropy `code_verifier`: 32 random bytes → 43 base64url chars,
     * comfortably inside RFC 7636's 43–128 range and identical in form to the
     * verifier the website generates for its own IdP layer.
     */
    fun newVerifier(): String = randomToken()

    /** A fresh opaque CSRF `state` (same entropy/shape as a verifier). */
    fun newState(): String = randomToken()

    /** `code_challenge` for [verifier] using the S256 method. */
    fun challengeOf(verifier: String): String {
        val digest = MessageDigest.getInstance("SHA-256").digest(verifier.toByteArray(Charsets.US_ASCII))
        return encoder.encodeToString(digest)
    }

    private fun randomToken(): String {
        val bytes = ByteArray(32)
        random.nextBytes(bytes)
        return encoder.encodeToString(bytes)
    }
}
