use scraper::{Html, Selector};

use crate::cookie::MangaDexCookie;
use crate::helpers::parse_manga_id_from_scraped_url;
use crate::types::{
    Demographic, HentaiMode, Language, MangaResultsSortBy, PublicationStatus, Tag, TagSearchMode,
};
use crate::web_scrape::responses::MangaScrapeInfo;
use crate::MangaDexClient;

/// Search for manga titles that match the query.
///
/// Note: Search is disabled for guests. Users must be logged in to use this feature.
pub struct SearchMangaScrapeBuilder<'a> {
    md_client: &'a (dyn MangaDexClient + 'a),
    page: u32,
    use_cover_thumbnail: bool,
    sort_by: Option<MangaResultsSortBy>,
    title: Option<String>,
    author: Option<String>,
    artist: Option<String>,
    /// Original publication language.
    original_language: Language,
    /// Empty denotes all demographics.
    demographic_types: Vec<Demographic>,
    /// Empty denotes all statuses.
    publication_statuses: Vec<PublicationStatus>,
    /// Tags that the manga must be tagged with.
    include_tags: Vec<Tag>,
    tag_inclusion_mode: TagSearchMode,
    /// Tags that the manga must *not* be tagged with.
    exclude_tags: Vec<Tag>,
    tag_exclusion_mode: TagSearchMode,
    hentai_mode: HentaiMode,
}

impl<'a> SearchMangaScrapeBuilder<'a> {
    /// Create a new builder instance for fetching the latest chapter updates.
    pub fn new(md_client: &'a dyn MangaDexClient) -> SearchMangaScrapeBuilder<'a> {
        Self {
            md_client,
            page: 1,
            use_cover_thumbnail: false,
            sort_by: None,
            title: None,
            author: None,
            artist: None,
            original_language: Language::All,
            demographic_types: Vec::new(),
            publication_statuses: Vec::new(),
            include_tags: Vec::new(),
            tag_inclusion_mode: TagSearchMode::All,
            exclude_tags: Vec::new(),
            tag_exclusion_mode: TagSearchMode::Any,
            hentai_mode: HentaiMode::Hide,
        }
    }

    /// Set the page to fetch.
    pub fn page(&mut self, page: u32) -> &mut Self {
        self.page = page;
        self
    }

    /// Choose to use the thumbnail version of the cover image.
    ///
    /// If false, it will use the full-resolution image.
    pub fn use_cover_thumbnail(&mut self, use_cover_thumbnail: bool) -> &mut Self {
        self.use_cover_thumbnail = use_cover_thumbnail;
        self
    }

    /// Order the results by the specified field.
    pub fn sort_by<T: Into<MangaResultsSortBy>>(&mut self, sort_by: T) -> &mut Self {
        self.sort_by = Some(sort_by.into());
        self
    }

    /// Look for the specified manga title.
    pub fn title<T: Into<String>>(&mut self, title: T) -> &mut Self {
        self.title = Some(title.into());
        self
    }

    /// Filter the search results by the author.
    pub fn author<T: Into<String>>(&mut self, author: T) -> &mut Self {
        self.author = Some(author.into());
        self
    }

    /// Filter the search results by the artist.
    pub fn artist<T: Into<String>>(&mut self, artist: T) -> &mut Self {
        self.artist = Some(artist.into());
        self
    }

    /// Filter the search results by the original publication language.
    pub fn original_language<T: Into<Language>>(&mut self, original_language: T) -> &mut Self {
        self.original_language = original_language.into();
        self
    }

    /// Filter the search results by demographic types.
    pub fn demographic_types(&mut self, demographic_types: Vec<Demographic>) -> &mut Self {
        self.demographic_types = demographic_types;
        self
    }

    /// Filter the results by publication statuses.
    pub fn publication_statuses(
        &mut self,
        publication_statuses: Vec<PublicationStatus>,
    ) -> &mut Self {
        self.publication_statuses = publication_statuses;
        self
    }

    /// Filter the results so that they contain the tags.
    pub fn include_tags(&mut self, include_tags: Vec<Tag>) -> &mut Self {
        self.include_tags = include_tags;
        self
    }

    /// Set the mode for searching the tags to include.
    ///
    /// "all (and)" returns results that match all the tags.
    /// "any (or)" returns results that match any of the tags.
    pub fn tag_inclusion_mode(&mut self, tag_inclusion_mode: TagSearchMode) -> &mut Self {
        self.tag_inclusion_mode = tag_inclusion_mode;
        self
    }

    /// Filter the results so that they don't contain the tags.
    pub fn exclude_tags(&mut self, exclude_tags: Vec<Tag>) -> &mut Self {
        self.exclude_tags = exclude_tags;
        self
    }

    /// Filter the results to include/exclude hentai titles.
    pub fn hentai_mode(&mut self, hentai_mode: HentaiMode) -> &mut Self {
        self.hentai_mode = hentai_mode;
        self
    }

    /// Set the mode for searching the tags to exclude.
    ///
    /// "all (and)" returns results that match all the tags.
    /// "any (or)" returns results that match any of the tags.
    pub fn tag_exclusion_mode(&mut self, tag_exclusion_mode: TagSearchMode) -> &mut Self {
        self.tag_exclusion_mode = tag_exclusion_mode;
        self
    }

    /// Send the request to MangaDex.
    pub async fn send(&self) -> Result<Vec<MangaScrapeInfo>, reqwest::Error> {
        let mut params = vec![
            ("p", self.page.to_string()),
            ("tag_mode_inc", self.tag_inclusion_mode.to_string()),
            ("tag_mode_exc", self.tag_exclusion_mode.to_string()),
        ];

        if let Some(sort_by) = &self.sort_by {
            params.push(("s", (*sort_by as u8).to_string()));
        }

        if let Some(title) = &self.title {
            params.push(("title", title.clone()));
        }

        if let Some(author) = &self.author {
            params.push(("author", author.clone()));
        }

        if let Some(artist) = &self.artist {
            params.push(("artist", artist.clone()));
        }

        if self.original_language != Language::All {
            params.push(("lang_id", (self.original_language as u8).to_string()))
        }

        if !self.demographic_types.is_empty() {
            params.push((
                "demos",
                self.demographic_types
                    .iter()
                    .map(|d| (*d as u8).to_string())
                    .collect::<Vec<String>>()
                    .join(","),
            ));
        }

        if !self.publication_statuses.is_empty() {
            params.push((
                "statuses",
                self.publication_statuses
                    .iter()
                    .map(|s| (*s as u8).to_string())
                    .collect::<Vec<String>>()
                    .join(","),
            ));
        }

        if !self.include_tags.is_empty() || !self.exclude_tags.is_empty() {
            let mut tags: Vec<String> = self
                .include_tags
                .iter()
                .map(|t| (*t as u8).to_string())
                .collect();

            let mut exclude_tags: Vec<String> = self
                .exclude_tags
                .iter()
                .filter_map(|t| {
                    let tag_id = (*t as u8).to_string();
                    if tags.contains(&tag_id) {
                        None
                    } else {
                        Some(format!("-{}", tag_id))
                    }
                })
                .collect();
            tags.append(&mut exclude_tags);

            params.push(("tags", tags.join(",")));
        }

        let mut req = self.md_client.client().get(&format!(
            "{base_url}/search",
            base_url = self.md_client.base_url(),
        ));

        match self.hentai_mode {
            HentaiMode::ShowAll | HentaiMode::ShowOnly => {
                req = req.header(
                    reqwest::header::COOKIE,
                    MangaDexCookie::HentaiToggle(self.hentai_mode).to_string(),
                );
            }
            HentaiMode::Hide => {}
        }

        let resp = req.query(&params).send().await?;

        let body = resp.text().await?;
        let fragment = Html::parse_document(&body);
        // Selecting the chapter URL is difficult since there are no unique identifiers.
        let title_selector = Selector::parse("a.manga_title").unwrap();

        Ok(fragment
            .select(&title_selector)
            .filter_map(|el| {
                let title: String = el.text().collect();
                let url = el.value().attr("href")?;
                let manga_id = parse_manga_id_from_scraped_url(url)?;

                Some(MangaScrapeInfo::new(
                    self.md_client.base_url(),
                    manga_id,
                    title,
                    self.use_cover_thumbnail,
                ))
            })
            .collect())
    }
}
