use clap::{Parser, Subcommand, ValueEnum};
use ddgs::{BooksOptions, Ddgs, NewsBackend, NewsOptions, SafeSearch, TextBackend, TextOptions};

#[derive(Debug, Parser)]
#[command(version, about = "Search DuckDuckGo-compatible sources from Rust")]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    Text(TextArgs),
    News(NewsArgs),
    Books(BookArgs),
}

#[derive(Debug, Parser)]
struct TextArgs {
    #[command(flatten)]
    search: SearchArgs,

    #[arg(long, value_enum, default_value_t = TextBackendArg::Auto)]
    backend: TextBackendArg,
}

#[derive(Debug, Parser)]
struct SearchArgs {
    query: String,

    #[arg(short, long)]
    max_results: Option<usize>,

    #[arg(long, default_value_t = 1)]
    page: usize,

    #[arg(long, value_enum, default_value_t = SafeSearchArg::Moderate)]
    safesearch: SafeSearchArg,
}

#[derive(Debug, Parser)]
struct NewsArgs {
    #[command(flatten)]
    search: SearchArgs,

    #[arg(long, value_enum, default_value_t = NewsBackendArg::Auto)]
    backend: NewsBackendArg,
}

#[derive(Debug, Parser)]
struct BookArgs {
    query: String,

    #[arg(short, long)]
    max_results: Option<usize>,

    #[arg(long, default_value_t = 1)]
    page: usize,
}

#[derive(Debug, Clone, Copy, ValueEnum)]
enum SafeSearchArg {
    On,
    Moderate,
    Off,
}

#[derive(Debug, Clone, Copy, ValueEnum)]
enum TextBackendArg {
    Auto,
    All,
    Api,
    Brave,
    Google,
    Grokipedia,
    Html,
    Lite,
    Mojeek,
    Startpage,
    Wikipedia,
    Yahoo,
    Yandex,
}

#[derive(Debug, Clone, Copy, ValueEnum)]
enum NewsBackendArg {
    Auto,
    All,
    Bing,
    Duckduckgo,
    Yahoo,
}

impl From<SafeSearchArg> for SafeSearch {
    fn from(value: SafeSearchArg) -> Self {
        match value {
            SafeSearchArg::On => Self::On,
            SafeSearchArg::Moderate => Self::Moderate,
            SafeSearchArg::Off => Self::Off,
        }
    }
}

impl From<TextBackendArg> for TextBackend {
    fn from(value: TextBackendArg) -> Self {
        match value {
            TextBackendArg::Auto => Self::Auto,
            TextBackendArg::All => Self::All,
            TextBackendArg::Api => Self::Api,
            TextBackendArg::Brave => Self::Brave,
            TextBackendArg::Google => Self::Google,
            TextBackendArg::Grokipedia => Self::Grokipedia,
            TextBackendArg::Html => Self::Html,
            TextBackendArg::Lite => Self::Lite,
            TextBackendArg::Mojeek => Self::Mojeek,
            TextBackendArg::Startpage => Self::Startpage,
            TextBackendArg::Wikipedia => Self::Wikipedia,
            TextBackendArg::Yahoo => Self::Yahoo,
            TextBackendArg::Yandex => Self::Yandex,
        }
    }
}

impl From<NewsBackendArg> for NewsBackend {
    fn from(value: NewsBackendArg) -> Self {
        match value {
            NewsBackendArg::Auto => Self::Auto,
            NewsBackendArg::All => Self::All,
            NewsBackendArg::Bing => Self::Bing,
            NewsBackendArg::Duckduckgo => Self::Duckduckgo,
            NewsBackendArg::Yahoo => Self::Yahoo,
        }
    }
}

#[tokio::main]
async fn main() -> ddgs::Result<()> {
    let cli = Cli::parse();
    let ddgs = Ddgs::new()?;

    match cli.command {
        Command::Text(args) => {
            let options = TextOptions::default()
                .page(args.search.page)
                .safesearch(args.search.safesearch.into())
                .backend(args.backend.into());
            let options = if let Some(max_results) = args.search.max_results {
                options.max_results(max_results)
            } else {
                options
            };
            print_json(&ddgs.text_with_options(args.search.query, options).await?);
        }
        Command::News(args) => {
            let options = NewsOptions::default()
                .page(args.search.page)
                .safesearch(args.search.safesearch.into())
                .backend(args.backend.into());
            let options = if let Some(max_results) = args.search.max_results {
                options.max_results(max_results)
            } else {
                options
            };
            print_json(&ddgs.news_with_options(args.search.query, options).await?);
        }
        Command::Books(args) => {
            let options = BooksOptions::default().page(args.page);
            let options = if let Some(max_results) = args.max_results {
                options.max_results(max_results)
            } else {
                options
            };
            print_json(&ddgs.books_with_options(args.query, options).await?);
        }
    }

    Ok(())
}

fn print_json<T: serde::Serialize>(value: &T) {
    println!(
        "{}",
        serde_json::to_string_pretty(value).expect("serializing search results should not fail")
    );
}
