//! PKCE (Proof Key for Code Exchange) helpers — pure functions with no side effects.
//!
//! Covers code verifier/challenge generation, base64url encoding,
//! URL percent-encoding, and authorization URL construction.

use sha2::{Digest, Sha256};

/// The Anthropic authorize endpoint.
const AUTHORIZE_URL: &str = "https://claude.com/cai/oauth/authorize";
/// The OAuth client ID used by Claude Code.
pub(crate) const CLIENT_ID: &str = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
/// The OAuth scopes requested.
pub const SCOPES: &str =
    "user:inference user:profile user:mcp_servers user:sessions:claude_code user:file_upload";

/// Generate a random alphanumeric string of the given length.
///
/// Used to produce PKCE code verifiers (128 chars) and CSRF state tokens (32 chars).
pub fn random_string(len: usize) -> String {
    use std::collections::hash_map::RandomState;
    use std::hash::{BuildHasher, Hasher};
    let mut s = String::with_capacity(len);
    let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    for _ in 0..len {
        let hasher = RandomState::new().build_hasher();
        let idx = hasher.finish() as usize % chars.len();
        s.push(chars[idx] as char);
    }
    s
}

/// Compute the S256 PKCE code challenge from a code verifier.
///
/// Returns the base64url-encoded SHA-256 hash of `verifier` (no padding).
pub fn compute_code_challenge(verifier: &str) -> String {
    let hash = Sha256::digest(verifier.as_bytes());
    base64url_encode(&hash)
}

/// Base64url-encode `data` without padding (RFC 7636).
pub fn base64url_encode(data: &[u8]) -> String {
    const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    let mut result = String::new();
    let mut i = 0;
    while i < data.len() {
        let b0 = data[i] as u32;
        let b1 = if i + 1 < data.len() {
            data[i + 1] as u32
        } else {
            0
        };
        let b2 = if i + 2 < data.len() {
            data[i + 2] as u32
        } else {
            0
        };
        let triple = (b0 << 16) | (b1 << 8) | b2;

        result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
        result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
        if i + 1 < data.len() {
            result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
        }
        if i + 2 < data.len() {
            result.push(CHARS[(triple & 0x3F) as usize] as char);
        }
        i += 3;
    }
    result.replace('+', "-").replace('/', "_")
}

/// Percent-encode `input` for use in URL query parameters (RFC 3986 unreserved chars).
pub fn percent_encode(input: &str) -> String {
    let mut encoded = String::with_capacity(input.len() * 3);
    for byte in input.bytes() {
        match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                encoded.push(byte as char);
            }
            _ => {
                encoded.push_str(&format!("%{byte:02X}"));
            }
        }
    }
    encoded
}

/// Build the full authorization URL to redirect the browser to.
///
/// `redirect_uri` — the callback URL (`http://localhost:<port>/callback`)
/// `code_challenge` — the S256 code challenge
/// `csrf_state` — the random CSRF state token
pub fn build_authorize_url(redirect_uri: &str, code_challenge: &str, csrf_state: &str) -> String {
    format!(
        "{}?code=true&client_id={}&response_type=code&redirect_uri={}&scope={}&code_challenge={}&code_challenge_method=S256&state={}",
        AUTHORIZE_URL,
        CLIENT_ID,
        percent_encode(redirect_uri),
        percent_encode(SCOPES),
        percent_encode(code_challenge),
        percent_encode(csrf_state),
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn base64url_encode_basic() {
        assert_eq!(base64url_encode(b"Hello"), "SGVsbG8");
    }

    #[test]
    fn base64url_encode_no_padding() {
        assert!(!base64url_encode(b"a").contains('='));
    }

    #[test]
    fn base64url_encode_no_plus_or_slash() {
        let data: Vec<u8> = (0..=255).collect();
        let encoded = base64url_encode(&data);
        assert!(!encoded.contains('+'));
        assert!(!encoded.contains('/'));
    }

    #[test]
    fn compute_code_challenge_returns_nonempty() {
        assert!(!compute_code_challenge("test_verifier").is_empty());
    }

    #[test]
    fn compute_code_challenge_is_deterministic() {
        assert_eq!(
            compute_code_challenge("same"),
            compute_code_challenge("same")
        );
    }

    #[test]
    fn random_string_length() {
        assert_eq!(random_string(64).len(), 64);
    }

    #[test]
    fn random_string_is_alphanumeric() {
        assert!(
            random_string(100)
                .chars()
                .all(|c| c.is_ascii_alphanumeric())
        );
    }

    #[test]
    fn percent_encode_unreserved_chars_unchanged() {
        assert_eq!(percent_encode("abc-_.~"), "abc-_.~");
    }

    #[test]
    fn percent_encode_space_becomes_percent_20() {
        assert_eq!(percent_encode("hello world"), "hello%20world");
    }

    #[test]
    fn build_authorize_url_contains_client_id() {
        let url = build_authorize_url("http://localhost:3001/callback", "challenge", "state");
        assert!(url.contains(CLIENT_ID));
    }

    #[test]
    fn build_authorize_url_contains_state() {
        let url = build_authorize_url("http://localhost:3001/callback", "challenge", "mystate");
        assert!(url.contains("mystate"));
    }
}
