use crate::models::{AudibleMetadata, CurrentMetadata, MatchCandidate, MatchConfidence, AudibleAuthor};
use anyhow::Result;
use console::style;
use inquire::{Confirm, CustomType, Select, Text};
pub enum UserChoice {
SelectMatch(usize), Skip, ManualEntry, CustomSearch, }
pub fn prompt_match_selection(
current: &CurrentMetadata,
candidates: &[MatchCandidate],
) -> Result<UserChoice> {
println!("\n{}", style("Match Candidates:").bold().cyan());
println!(
"Current: {} by {}",
current.title.as_deref().unwrap_or("Unknown"),
current.author.as_deref().unwrap_or("Unknown")
);
println!();
let mut options = Vec::new();
for (i, candidate) in candidates.iter().enumerate() {
let percentage = (1.0 - candidate.distance.total_distance()) * 100.0;
let color_fn: fn(String) -> String = match candidate.confidence {
MatchConfidence::Strong => style_green,
MatchConfidence::Medium => style_yellow,
MatchConfidence::Low => style_red,
MatchConfidence::None => style_dim,
};
let label = format!(
"{}. [{:>5.1}%] {} by {} ({}, {})",
i + 1,
percentage,
candidate.metadata.title,
candidate
.metadata
.authors
.first()
.map(|a| a.name.as_str())
.unwrap_or("Unknown"),
candidate
.metadata
.published_year
.map(|y| y.to_string())
.unwrap_or_else(|| "N/A".to_string()),
format_duration(candidate.metadata.runtime_length_ms),
);
options.push(color_fn(label));
}
options.push(style("───────────────────────────────────").dim().to_string());
options.push(style("[S] Skip this file").yellow().to_string());
options.push(style("[M] Enter metadata manually").cyan().to_string());
options.push(style("[R] Search with different terms").blue().to_string());
let selection = Select::new("Select an option:", options)
.with_page_size(15)
.prompt()?;
for (idx, ch) in selection.chars().enumerate() {
if ch.is_ascii_digit() {
let digit = ch.to_digit(10).unwrap() as usize;
if digit >= 1 && digit <= candidates.len() {
return Ok(UserChoice::SelectMatch(digit - 1));
}
break;
}
if idx >= 20 {
break;
}
}
if selection.contains("[S]") {
Ok(UserChoice::Skip)
} else if selection.contains("[M]") {
Ok(UserChoice::ManualEntry)
} else if selection.contains("[R]") {
Ok(UserChoice::CustomSearch)
} else {
Ok(UserChoice::Skip) }
}
pub fn confirm_match(
current: &CurrentMetadata,
selected: &MatchCandidate,
) -> Result<bool> {
println!("\n{}", style("Metadata Changes:").bold().cyan());
println!();
show_field_change(
"Title",
current.title.as_deref(),
Some(&selected.metadata.title),
);
show_field_change(
"Author",
current.author.as_deref(),
selected
.metadata
.authors
.first()
.map(|a| a.name.as_str()),
);
if let Some(subtitle) = &selected.metadata.subtitle {
show_field_change("Subtitle", None, Some(subtitle));
}
if let Some(narrator) = selected.metadata.narrators.first() {
show_field_change("Narrator", None, Some(narrator));
}
show_field_change(
"Year",
current.year.as_ref().map(|y| y.to_string()).as_deref(),
selected
.metadata
.published_year
.as_ref()
.map(|y| y.to_string())
.as_deref(),
);
if let Some(publisher) = &selected.metadata.publisher {
show_field_change("Publisher", None, Some(publisher));
}
println!();
Ok(Confirm::new("Apply these changes?")
.with_default(true)
.prompt()?)
}
pub fn prompt_manual_metadata() -> Result<AudibleMetadata> {
println!("\n{}", style("Enter Metadata Manually:").bold().cyan());
let title = Text::new("Title:").prompt()?;
let author_name = Text::new("Author:").prompt()?;
let narrator = Text::new("Narrator (optional):")
.with_default("")
.prompt()?;
let year: Option<u32> = CustomType::new("Year (optional):")
.with_error_message("Please enter a valid year or leave empty")
.prompt_skippable()?;
Ok(AudibleMetadata {
asin: String::from("manual"),
title,
subtitle: None,
authors: vec![AudibleAuthor {
asin: None,
name: author_name,
}],
narrators: if narrator.is_empty() {
vec![]
} else {
vec![narrator]
},
publisher: None,
published_year: year,
description: None,
cover_url: None,
isbn: None,
genres: vec![],
tags: vec![],
series: vec![],
language: None,
runtime_length_ms: None,
rating: None,
is_abridged: None,
})
}
pub fn prompt_custom_search() -> Result<(Option<String>, Option<String>)> {
println!("\n{}", style("Custom Search:").bold().cyan());
let title = Text::new("Title (optional):")
.with_default("")
.prompt()?;
let author = Text::new("Author (optional):")
.with_default("")
.prompt()?;
let title_opt = if title.is_empty() {
None
} else {
Some(title)
};
let author_opt = if author.is_empty() {
None
} else {
Some(author)
};
Ok((title_opt, author_opt))
}
fn show_field_change(field: &str, old: Option<&str>, new: Option<&str>) {
let old_display = old.unwrap_or("(none)");
let new_display = new.unwrap_or("(none)");
if old != new {
println!(
" {}: {} → {}",
style(field).bold(),
style(old_display).dim(),
style(new_display).green()
);
} else {
println!(
" {}: {}",
style(field).bold(),
style(new_display).dim()
);
}
}
fn format_duration(ms: Option<u64>) -> String {
match ms {
Some(ms) => {
let hours = ms / 3_600_000;
let minutes = (ms % 3_600_000) / 60_000;
format!("{}h {}m", hours, minutes)
}
None => "N/A".to_string(),
}
}
fn style_green(s: String) -> String {
style(s).green().to_string()
}
fn style_yellow(s: String) -> String {
style(s).yellow().to_string()
}
fn style_red(s: String) -> String {
style(s).red().to_string()
}
fn style_dim(s: String) -> String {
style(s).dim().to_string()
}