use crate::cache::CacheService;
use crate::config::CONFIG;
use crate::error::EngineError;
use crate::format::format_result;
use crate::models::{Identifier, ProblemSummary, UserDetail};
use crate::services::submission::SubmissionService;
use crate::{client::LeetCodeClient, models::Language};
use std::fs;
use std::path::Path;
#[derive(Clone)]
pub struct Picker {
pub client: LeetCodeClient,
}
impl Picker {
pub fn new(client: LeetCodeClient) -> Self {
Picker { client }
}
pub async fn pick(
&self,
identifier: &Identifier,
language: &Option<Language>,
) -> crate::error::Result<(String, String)> {
let mut language = match language {
Some(lang) => lang.clone(),
None => {
let config = CONFIG.get().expect("Config not initialised");
if let Some(lang) = &config.language {
Language::from(lang)
} else {
println!("🔤 No language specified, defaulting to Python.");
Language::Python
}
}
};
if let Identifier::String(ident) = identifier {
let snake_slug = ident.replace("-", "_");
let code_filename = format!("{}.{}", snake_slug, language.code_extension());
let desc_filename = format!("{}.md", snake_slug);
if Path::new(&code_filename).exists() && Path::new(&desc_filename).exists() {
return Ok((code_filename, desc_filename));
}
}
let question = match identifier {
Identifier::Number(num) => {
println!("🔍 Fetching problem ID: {}...", num);
self.client.get_question_by_id(*num).await?
}
Identifier::String(identifier) => {
println!("🔍 Fetching problem: {}...", identifier);
self.client.get_question_by_slug(identifier).await?
}
};
let formatted_content = html2md::parse_html(&question.content);
let md_content = format!("# {}\n\n{}", question.title, formatted_content);
let snippet = question
.code_snippets
.iter()
.find(|s| s.lang_slug == language.to_lang_slug());
let snippet = match snippet {
Some(s) => s,
None => {
let snippet = question.code_snippets.first().ok_or_else(|| {
EngineError::Other("LeetCode problem has no code snippets".to_string())
})?;
language = Language::from(snippet.lang_slug.clone());
snippet
}
};
let snake_slug = question.title_slug.replace("-", "_");
let code_filename = format!("{}.{}", snake_slug, language.code_extension());
let desc_filename = format!("{}.md", snake_slug);
let meta = format!(
"{} id={} slug={} lang={}",
language.meta_comment_prefix(),
question.question_id,
question.title_slug,
language.to_lang_slug()
);
if let Err(e) = fs::write(&code_filename, format!("{}\n\n{}", meta, snippet.code)) {
eprintln!("❌ failed to write code file: {}", e);
return Err(EngineError::System);
}
if let Err(e) = fs::write(&desc_filename, md_content) {
eprintln!("❌ failed to write description file: {}", e);
return Err(EngineError::System);
}
println!("✅ files generated successfully.");
Ok((code_filename, desc_filename))
}
pub async fn test_submit(&self, file: &str) {
let service = SubmissionService::new(self.client.clone());
match service.submit_or_test(file, true).await {
Ok(result) => format_result(&result),
Err(e) => eprintln!("❌ {}", e),
}
}
pub async fn submit(&self, file: &str) {
let service = SubmissionService::new(self.client.clone());
match service.submit_or_test(file, false).await {
Ok(result) => format_result(&result),
Err(e) => eprintln!("❌ {}", e),
}
}
pub async fn get_user_data(&self) -> crate::error::Result<UserDetail> {
let cache = CacheService::new();
let user_path = cache.user_path();
let data = match fs::read_to_string(&user_path) {
Ok(v) => {
let client = self.client.clone();
let user_path_bg = user_path.clone();
tokio::spawn(async move {
let result: Result<(), Box<dyn std::error::Error>> = async {
let user_detail = client.get_user_detail().await?;
let data = serde_json::to_string(&user_detail)?;
let _ = fs::write(&user_path_bg, &data);
Ok(())
}
.await;
let _ = result;
});
v
}
Err(_) => {
let user_detail = self.client.get_user_detail().await?;
let data = serde_json::to_string(&user_detail)?;
let _ = fs::write(&user_path, &data);
data
}
};
let user_detail: UserDetail = serde_json::from_str(&data).map_err(|e| {
eprintln!("Failed to parse user details: {}", e);
eprintln!("Try running `leetrs tui` again to refresh the cache.");
if let Err(err) = fs::remove_file(&user_path) {
eprintln!("Failed to remove corrupted cache file: {}", err);
}
e
})?;
Ok(user_detail)
}
pub async fn list_problems(&self) -> crate::error::Result<Vec<ProblemSummary>> {
let cache = CacheService::new();
let data_path = cache.problems_path();
let user_path = cache.user_path();
let data = match fs::read_to_string(&data_path) {
Ok(v) => {
let client_clone = self.client.clone();
let data_path_bg = data_path.clone();
let user_path_bg = user_path.clone();
tokio::spawn(async move {
let res: Result<(), Box<dyn std::error::Error + Send + Sync>> = async {
let user_detail = client_clone.get_user_detail().await?;
let data = serde_json::to_string(&user_detail)?;
let _ = fs::write(&user_path_bg, data);
let mut problems = client_clone.get_problem_list().await?;
let question_tags = client_clone.get_topics_question_list().await?;
for question_tag in question_tags {
question_tag.question_ids.iter().for_each(|question_id| {
if let Some(problem) =
problems.iter_mut().find(|p| p.id == *question_id)
{
problem.topics.push(question_tag.name.clone());
}
});
}
let data = serde_json::to_string(&problems)?;
let _ = fs::write(&data_path_bg, data);
Ok(())
}
.await;
let _ = res;
});
v
}
Err(_) => {
let mut problems = self.client.get_problem_list().await?;
let question_tags = self.client.get_topics_question_list().await?;
for question_tag in question_tags {
question_tag.question_ids.iter().for_each(|question_id| {
if let Some(problem) = problems.iter_mut().find(|p| p.id == *question_id) {
problem.topics.push(question_tag.name.clone());
}
});
}
let data = serde_json::to_string(&problems)?;
let _ = fs::write(&data_path, &data);
data
}
};
let problems: Vec<ProblemSummary> = serde_json::from_str(&data).map_err(|e| {
eprintln!("Failed to parse problem list: {}", e);
eprintln!("Try running `leetrs tui` again to refresh the cache.");
if let Err(err) = fs::remove_file(&data_path) {
eprintln!("Failed to remove corrupted cache file: {}", err);
}
e
})?;
Ok(problems)
}
}