//! OAuth I/O — the ONLY place in `service/oauth/` that may perform side effects.
//!
//! Side effects here include: reading the system clock, making HTTP requests to
//! the Anthropic token endpoint, and reading/writing `~/.claude/.credentials.json`.
//! All business logic and branching belong in `mod.rs`, `pkce.rs`, or `flow.rs`.

use super::Error;
use super::flow::{AccountInfo, build_account_info};
use super::pkce::SCOPES;
use crate::llm::oauth::{self, CredentialsFile, PoolAccount};
use crate::slog;

/// Raw token exchange result returned by the Anthropic OAuth endpoint.
#[derive(serde::Deserialize)]
pub(super) struct TokenExchangeResult {
    pub access_token: String,
    pub refresh_token: Option<String>,
    pub expires_in: u64,
    #[allow(dead_code)]
    pub token_type: Option<String>,
    #[allow(dead_code)]
    pub scope: Option<String>,
}

/// Return the current Unix-epoch time in milliseconds.
pub(super) fn current_time_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// Exchange an authorization code for tokens via the Anthropic token endpoint.
///
/// Returns the raw token response on success. Network or HTTP errors are
/// mapped to typed [`Error`] variants.
pub(super) async fn exchange_code_for_tokens(
    code: &str,
    redirect_uri: &str,
    code_verifier: &str,
    csrf_state: &str,
) -> Result<TokenExchangeResult, Error> {
    use super::pkce::CLIENT_ID;
    const TOKEN_ENDPOINT: &str = "https://platform.claude.com/v1/oauth/token";

    slog!("[oauth] Exchanging authorization code for tokens");

    let client = reqwest::Client::new();
    let resp = client
        .post(TOKEN_ENDPOINT)
        .header("Content-Type", "application/json")
        .json(&serde_json::json!({
            "grant_type": "authorization_code",
            "code": code,
            "client_id": CLIENT_ID,
            "redirect_uri": redirect_uri,
            "code_verifier": code_verifier,
            "state": csrf_state,
        }))
        .send()
        .await
        .map_err(|e| Error::Network(format!("Failed to contact Anthropic: {e}")))?;

    let status = resp.status();
    let body = resp.text().await.unwrap_or_default();

    slog!(
        "[oauth] Token exchange response (HTTP {}): {}",
        status,
        body
    );

    if !status.is_success() {
        return Err(Error::InvalidGrant(format!(
            "Anthropic returned HTTP {status}. Please try again."
        )));
    }

    serde_json::from_str(&body)
        .map_err(|e| Error::Parse(format!("Unexpected response from Anthropic: {e}")))
}

/// Persist a token exchange result to `~/.claude/.credentials.json` and to the
/// multi-account pool (`~/.claude/oauth_pool.json`).
///
/// Writes the credentials file for Claude Code CLI compatibility, then upserts
/// the account into the pool keyed by `email`. If `email` is empty a fallback
/// key is derived from the first 16 characters of the access token.
pub(super) fn save_credentials(
    token: &TokenExchangeResult,
    now_ms: u64,
    email: &str,
) -> Result<(), Error> {
    let expires_at = now_ms + (token.expires_in * 1000);
    let scopes: Vec<String> = SCOPES.split(' ').map(|s| s.to_string()).collect();

    // Write to .credentials.json for Claude Code CLI compatibility.
    let creds = CredentialsFile {
        claude_ai_oauth: oauth::OAuthCredentials {
            access_token: token.access_token.clone(),
            refresh_token: token.refresh_token.clone().unwrap_or_default(),
            expires_at,
            scopes: scopes.clone(),
            subscription_type: None,
            rate_limit_tier: None,
        },
    };
    oauth::write_credentials(&creds).map_err(Error::TokenStorage)?;

    // Build the pool key: prefer email, fall back to token prefix.
    let key = if email.is_empty() {
        let prefix = token
            .access_token
            .get(..token.access_token.len().min(16))
            .unwrap_or(&token.access_token);
        format!("account-{prefix}")
    } else {
        email.to_string()
    };

    let account = PoolAccount {
        email: key,
        access_token: token.access_token.clone(),
        refresh_token: token.refresh_token.clone().unwrap_or_default(),
        expires_at,
        scopes,
        subscription_type: None,
        rate_limit_tier: None,
        rate_limited: false,
        rate_limit_reset_at: None,
    };
    oauth::upsert_pool_account(account).map_err(Error::TokenStorage)?;

    Ok(())
}

/// Attempt to fetch the authenticated user's email from Anthropic's userinfo endpoint.
///
/// Uses the access token as a Bearer credential. Returns `None` when the
/// request fails or the response does not contain an email field.
pub(super) async fn fetch_user_email(access_token: &str) -> Option<String> {
    const USERINFO_URL: &str = "https://claude.ai/api/me";

    #[derive(serde::Deserialize)]
    struct UserInfo {
        email: Option<String>,
    }

    let client = reqwest::Client::new();
    let resp = client
        .get(USERINFO_URL)
        .header("Authorization", format!("Bearer {access_token}"))
        .send()
        .await
        .ok()?;

    if !resp.status().is_success() {
        return None;
    }

    let info: UserInfo = resp.json().await.ok()?;
    info.email.filter(|e| !e.is_empty())
}

/// Load all accounts from the pool and compute their [`AccountInfo`].
///
/// Falls back to reading the legacy `.credentials.json` when the pool is empty
/// or missing, synthesising a single unauthenticated-or-authenticated entry so
/// existing deployments that have not yet run a new login continue to work.
pub(super) fn load_all_accounts() -> Vec<AccountInfo> {
    let now_ms = current_time_ms();

    match oauth::read_pool() {
        Ok(pool) if !pool.accounts.is_empty() => {
            let mut accounts: Vec<AccountInfo> = pool
                .accounts
                .values()
                .map(|a| build_account_info(a, now_ms))
                .collect();
            accounts.sort_by(|a, b| a.email.cmp(&b.email));
            accounts
        }
        _ => {
            // Pool empty or unreadable — fall back to legacy single-account file.
            match oauth::read_credentials() {
                Ok(creds) => {
                    let status = super::flow::build_flow_status(&creds, now_ms);
                    vec![AccountInfo {
                        email: "default".to_string(),
                        status: if status.expired {
                            super::flow::AccountStatus::Expired
                        } else {
                            super::flow::AccountStatus::Active
                        },
                        expires_at: status.expires_at,
                        has_refresh_token: status.has_refresh_token,
                    }]
                }
                Err(_) => vec![],
            }
        }
    }
}
