//! OAuth flow state types and pure decision logic.
//!
//! All functions here are pure — no I/O, no network, no clocks.
//! Side-effectful operations live exclusively in `io.rs`.

use crate::llm::oauth::{CredentialsFile, PoolAccount};

// ── Account pool status types ─────────────────────────────────────────────────

/// Status of a single account in the login pool as returned by `GET /oauth/status`.
#[derive(Debug, Clone, serde::Serialize)]
pub struct AccountInfo {
    /// Email address that identifies the account.
    pub email: String,
    /// Human-readable account status.
    pub status: AccountStatus,
    /// Unix-epoch millisecond expiry timestamp.
    pub expires_at: u64,
    /// Whether a non-empty refresh token is present.
    pub has_refresh_token: bool,
}

/// The status of a stored OAuth account.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum AccountStatus {
    /// Token is valid and not rate-limited.
    Active,
    /// Token expiry is in the past.
    Expired,
    /// A rate-limit response was observed for this account.
    RateLimited,
}

/// Build an [`AccountInfo`] from a [`PoolAccount`] and the current time.
pub fn build_account_info(account: &PoolAccount, now_ms: u64) -> AccountInfo {
    let status = if account.rate_limited {
        AccountStatus::RateLimited
    } else if is_token_expired(account.expires_at, now_ms) {
        AccountStatus::Expired
    } else {
        AccountStatus::Active
    };
    AccountInfo {
        email: account.email.clone(),
        status,
        expires_at: account.expires_at,
        has_refresh_token: !account.refresh_token.is_empty(),
    }
}

// ── PKCE flow ─────────────────────────────────────────────────────────────────

/// A pending PKCE flow waiting for an OAuth callback.
pub struct PendingFlow {
    /// The PKCE code verifier generated at flow initiation.
    pub code_verifier: String,
    /// The redirect URI sent to the authorization endpoint.
    pub redirect_uri: String,
}

/// Current OAuth credential status, computed without I/O from already-loaded credentials.
#[derive(Debug, Clone)]
pub struct FlowStatus {
    /// Whether valid credentials were found on disk.
    #[allow(dead_code)]
    pub authenticated: bool,
    /// Whether the access token is past its expiry timestamp.
    pub expired: bool,
    /// The Unix-epoch millisecond expiry timestamp (0 when unauthenticated).
    pub expires_at: u64,
    /// Whether a non-empty refresh token is present.
    pub has_refresh_token: bool,
}

/// Determine whether `expires_at` (Unix epoch ms) has passed, given `now_ms`.
///
/// Returns `true` when `now_ms > expires_at`.
pub fn is_token_expired(expires_at: u64, now_ms: u64) -> bool {
    now_ms > expires_at
}

/// Build a `FlowStatus` from loaded credentials and the current time.
pub fn build_flow_status(creds: &CredentialsFile, now_ms: u64) -> FlowStatus {
    let expires_at = creds.claude_ai_oauth.expires_at;
    FlowStatus {
        authenticated: true,
        expired: is_token_expired(expires_at, now_ms),
        expires_at,
        has_refresh_token: !creds.claude_ai_oauth.refresh_token.is_empty(),
    }
}

/// Return the unauthenticated `FlowStatus` (no credentials on disk).
#[allow(dead_code)]
pub fn unauthenticated_status() -> FlowStatus {
    FlowStatus {
        authenticated: false,
        expired: false,
        expires_at: 0,
        has_refresh_token: false,
    }
}

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

    #[test]
    fn is_token_expired_when_past_expiry() {
        assert!(is_token_expired(1000, 2000));
    }

    #[test]
    fn is_token_not_expired_when_before_expiry() {
        assert!(!is_token_expired(2000, 1000));
    }

    #[test]
    fn is_token_not_expired_at_exact_boundary() {
        // expires_at == now_ms → not expired
        assert!(!is_token_expired(1000, 1000));
    }

    #[test]
    fn unauthenticated_status_is_not_authenticated() {
        let s = unauthenticated_status();
        assert!(!s.authenticated);
        assert!(!s.expired);
        assert_eq!(s.expires_at, 0);
        assert!(!s.has_refresh_token);
    }

    #[test]
    fn build_flow_status_authenticated_not_expired() {
        use crate::llm::oauth::{CredentialsFile, OAuthCredentials};
        let creds = CredentialsFile {
            claude_ai_oauth: OAuthCredentials {
                access_token: "tok".to_string(),
                refresh_token: "ref".to_string(),
                expires_at: 5000,
                scopes: vec![],
                subscription_type: None,
                rate_limit_tier: None,
            },
        };
        let status = build_flow_status(&creds, 1000);
        assert!(status.authenticated);
        assert!(!status.expired);
        assert_eq!(status.expires_at, 5000);
        assert!(status.has_refresh_token);
    }

    #[test]
    fn build_flow_status_authenticated_expired() {
        use crate::llm::oauth::{CredentialsFile, OAuthCredentials};
        let creds = CredentialsFile {
            claude_ai_oauth: OAuthCredentials {
                access_token: "tok".to_string(),
                refresh_token: String::new(),
                expires_at: 1000,
                scopes: vec![],
                subscription_type: None,
                rate_limit_tier: None,
            },
        };
        let status = build_flow_status(&creds, 9999);
        assert!(status.authenticated);
        assert!(status.expired);
        assert!(!status.has_refresh_token);
    }
}
