use std::time::Duration;

use reqwest::header::{ACCEPT, ACCEPT_LANGUAGE, HeaderMap, HeaderValue, USER_AGENT};

use crate::books::{self, BookResult};
use crate::error::{DdgsError, Result};
use crate::news::{self, NewsResult};
use crate::options::{BooksOptions, NewsBackend, NewsOptions, TextBackend, TextOptions};
use crate::text::{self, TextResult};

#[derive(Clone, Debug)]
pub struct Ddgs {
    http: reqwest::Client,
}

impl Ddgs {
    pub fn new() -> Result<Self> {
        let mut headers = HeaderMap::new();
        headers.insert(
            USER_AGENT,
            HeaderValue::from_static(
                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 \
                 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
            ),
        );
        headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
        headers.insert(ACCEPT_LANGUAGE, HeaderValue::from_static("en-US,en;q=0.9"));

        let http = reqwest::Client::builder()
            .default_headers(headers)
            .timeout(Duration::from_secs(20))
            .redirect(reqwest::redirect::Policy::limited(10))
            .build()?;

        Ok(Self { http })
    }

    pub fn with_client(http: reqwest::Client) -> Self {
        Self { http }
    }

    pub async fn text(&self, query: impl AsRef<str>) -> Result<Vec<TextResult>> {
        self.text_with_options(query, TextOptions::default()).await
    }

    pub async fn text_with_options(
        &self,
        query: impl AsRef<str>,
        options: TextOptions,
    ) -> Result<Vec<TextResult>> {
        validate_query(query.as_ref())?;

        match options.backend {
            TextBackend::Auto | TextBackend::All => {
                text::search_auto(&self.http, query.as_ref(), &options).await
            }
            TextBackend::Api => text::search_api(&self.http, query.as_ref(), &options).await,
            TextBackend::Brave => text::search_brave(&self.http, query.as_ref(), &options).await,
            TextBackend::Google => text::search_google(&self.http, query.as_ref(), &options).await,
            TextBackend::Grokipedia => {
                text::search_grokipedia(&self.http, query.as_ref(), &options).await
            }
            TextBackend::Html => text::search_html(&self.http, query.as_ref(), &options).await,
            TextBackend::Lite => text::search_lite(&self.http, query.as_ref(), &options).await,
            TextBackend::Mojeek => text::search_mojeek(&self.http, query.as_ref(), &options).await,
            TextBackend::Startpage => {
                text::search_startpage(&self.http, query.as_ref(), &options).await
            }
            TextBackend::Wikipedia => {
                text::search_wikipedia(&self.http, query.as_ref(), &options).await
            }
            TextBackend::Yahoo => text::search_yahoo(&self.http, query.as_ref(), &options).await,
            TextBackend::Yandex => text::search_yandex(&self.http, query.as_ref(), &options).await,
        }
    }

    pub async fn news(&self, query: impl AsRef<str>) -> Result<Vec<NewsResult>> {
        self.news_with_options(query, NewsOptions::default()).await
    }

    pub async fn news_with_options(
        &self,
        query: impl AsRef<str>,
        options: NewsOptions,
    ) -> Result<Vec<NewsResult>> {
        validate_query(query.as_ref())?;
        match options.backend {
            NewsBackend::Auto | NewsBackend::All => {
                news::search_auto(&self.http, query.as_ref(), &options).await
            }
            NewsBackend::Bing => news::search_bing(&self.http, query.as_ref(), &options).await,
            NewsBackend::Duckduckgo => news::search(&self.http, query.as_ref(), &options).await,
            NewsBackend::Yahoo => news::search_yahoo(&self.http, query.as_ref(), &options).await,
        }
    }

    pub async fn books(&self, query: impl AsRef<str>) -> Result<Vec<BookResult>> {
        self.books_with_options(query, BooksOptions::default())
            .await
    }

    pub async fn books_with_options(
        &self,
        query: impl AsRef<str>,
        options: BooksOptions,
    ) -> Result<Vec<BookResult>> {
        validate_query(query.as_ref())?;
        books::search(&self.http, query.as_ref(), &options).await
    }
}

impl Default for Ddgs {
    fn default() -> Self {
        Self::new().expect("default HTTP client should be constructible")
    }
}

fn validate_query(query: &str) -> Result<()> {
    if query.trim().is_empty() {
        return Err(DdgsError::InvalidArgument(
            "query must not be empty".to_string(),
        ));
    }

    Ok(())
}
