use std::{io::Write as _, path::Path};
use chrono::Utc;
use crate::{common::client::Client, auth::{config::AuthConfig, oauth::OAuthApi, store::TokenStore}};
const TOKEN_FILE: &str = "schwab_tokens.json";
pub struct Provider {
store: TokenStore,
api: OAuthApi,
}
impl Provider {
pub fn new() -> Self {
let config = AuthConfig::new();
let api = OAuthApi::new(config);
let store = TokenStore::new(TOKEN_FILE);
Self {
api,
store,
}
}
pub async fn login(&self) -> anyhow::Result<()> {
if !Path::new(TOKEN_FILE).exists() {
println!("=== INITIAL SCHWAB OAUTH SETUP ===");
println!("1. Click this URL and log in:\n{}\n", self.api.generate_auth_url());
print!("2. Paste the full redirect URL (starts with https://127.0.0.1...): ");
std::io::stdout().flush()?;
let mut returned_url = String::new();
std::io::stdin().read_line(&mut returned_url)?;
let returned_url = returned_url.trim();
let code = self.api.extract_code_from_url(returned_url)?;
println!("Exchanging code for permanent tokens...");
let token = self.api.exchange_code_for_tokens(&code).await?;
self.store.save(&token)?;
println!("✅ Tokens saved to schwab_tokens.json successfully!");
} else {
println!("Checking/Verifying existing token pipeline...");
if let Some(token) = self.get_valid_token().await {
let truncated = if token.len() > 10 { &token[..10] } else { &token };
println!("Active Access Token: {}...[TRUNCATED]", truncated);
}
}
Ok(())
}
async fn get_valid_token(&self) -> Option<String> {
let Some(mut token_data) = self.store.load().await else {
return None;
};
if Utc::now().timestamp() >= (token_data.expires_at - 120) {
token_data = self.api.refresh_access_token(&token_data.refresh_token).await.ok()?;
}
Some(token_data.access_token)
}
pub async fn client(&self) -> anyhow::Result<Client> {
let Some(token) = self.get_valid_token().await else {
anyhow::bail!("Please login first");
};
Ok(Client::new(&token))
}
}