use regex::Regex;
use serde::{Deserialize, Serialize};
use url::Url;

use crate::error::Result;
use crate::html::{clean_html, first_capture};
use crate::options::BooksOptions;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BookResult {
    pub title: String,
    pub url: String,
    pub author: Option<String>,
    pub authors: Vec<String>,
    pub info: Option<String>,
    pub year: Option<String>,
    pub publisher: Option<String>,
    pub extension: Option<String>,
    pub pages: Option<String>,
    pub size: Option<String>,
    pub language: Option<String>,
    pub thumbnail: Option<String>,
}

pub async fn search(
    http: &reqwest::Client,
    query: &str,
    options: &BooksOptions,
) -> Result<Vec<BookResult>> {
    let html = fetch_books_html(http, query, options.page).await?;

    let mut results = parse_books_html(&html);
    if let Some(max_results) = options.max_results {
        results.truncate(max_results);
    }

    Ok(results)
}

async fn fetch_books_html(http: &reqwest::Client, query: &str, page: usize) -> Result<String> {
    let mut last_error = None;
    for base_url in [
        "https://annas-archive.org",
        "https://annas-archive.gd",
        "https://annas-archive.gl",
        "https://annas-archive.pk",
    ] {
        match http.get(build_url(base_url, query, page)?).send().await {
            Ok(response) => match response.error_for_status() {
                Ok(response) => return Ok(response.text().await?),
                Err(error) => last_error = Some(error),
            },
            Err(error) => last_error = Some(error),
        }
    }

    Err(last_error
        .expect("at least one Anna's Archive base URL should be attempted")
        .into())
}

fn build_url(base_url: &str, query: &str, page: usize) -> Result<Url> {
    let mut url = Url::parse(base_url)?.join("/search")?;
    url.query_pairs_mut()
        .append_pair("q", query)
        .append_pair("page", &page.max(1).to_string());
    Ok(url)
}

pub fn parse_books_html(html: &str) -> Vec<BookResult> {
    let href_re =
        Regex::new(r#"(?is)<a(?P<attrs>[^>]+href="(?P<href>[^"]+)"[^>]*)>(?P<body>.*?)</a>"#)
            .expect("valid href regex");
    let img_re = Regex::new(r#"(?is)<img[^>]+src="([^"]+)""#).expect("valid image regex");

    let mut results = Vec::new();
    for block in book_blocks(html) {
        let Some(title_link) = href_re.captures_iter(block).find(|capture| {
            let attrs = capture
                .name("attrs")
                .map(|value| value.as_str())
                .unwrap_or_default();
            let href = capture
                .name("href")
                .map(|value| value.as_str())
                .unwrap_or_default();
            attrs.contains("text-lg") || href.contains("/md5/")
        }) else {
            continue;
        };

        let href = title_link
            .name("href")
            .map(|value| value.as_str())
            .unwrap_or_default();
        let title_html = title_link
            .name("body")
            .map(|value| value.as_str())
            .unwrap_or_default();
        let title = clean_html(title_html);
        if title.is_empty() {
            continue;
        }

        let details = clean_html(block);
        let authors = parse_authors(block, &details);
        let publisher =
            parse_link_by_icon(block, "company").or_else(|| labeled_value(&details, "Publisher"));
        let info =
            parse_info(block).or_else(|| Some(details.clone()).filter(|value| !value.is_empty()));
        results.push(BookResult {
            title,
            url: absolute_archive_url(href),
            author: authors.first().cloned(),
            authors,
            info,
            year: first_capture(&details, r"\b(1[5-9]\d{2}|20\d{2})\b"),
            publisher,
            extension: labeled_value(&details, "Extension"),
            pages: labeled_value(&details, "Pages"),
            size: labeled_value(&details, "Size"),
            language: labeled_value(&details, "Language"),
            thumbnail: img_re
                .captures(block)
                .and_then(|capture| capture.get(1))
                .map(|value| absolute_archive_url(value.as_str())),
        });
    }

    results
}

fn book_blocks(html: &str) -> Vec<&str> {
    let marker_re = Regex::new(
        r#"(?is)<div[^>]+class="[^"]*(?:record-list-outer|h-\[125|js-vim-focus)[^"]*"[^>]*>"#,
    )
    .expect("valid book block marker regex");
    let starts: Vec<usize> = marker_re
        .find_iter(html)
        .map(|match_| match_.start())
        .collect();

    starts
        .iter()
        .enumerate()
        .map(|(index, start)| {
            let end = starts.get(index + 1).copied().unwrap_or(html.len());
            &html[*start..end]
        })
        .collect()
}

fn absolute_archive_url(value: &str) -> String {
    if value.starts_with("http://") || value.starts_with("https://") {
        return value.to_string();
    }

    format!("https://annas-archive.org{}", value)
}

fn labeled_value(details: &str, label: &str) -> Option<String> {
    let pattern = format!(
        r"(?i)\b{}\s*:\s*(.*?)\s*(?:\s[[:alpha:]][[:alpha:] ]{{1,24}}:\s*|[|•]|\z)",
        regex::escape(label)
    );
    first_capture(details, &pattern).map(|value| value.trim().to_string())
}

fn parse_authors(block: &str, details: &str) -> Vec<String> {
    parse_link_by_icon(block, "user")
        .or_else(|| labeled_value(details, "Author"))
        .or_else(|| labeled_value(details, "Authors"))
        .map(|value| {
            value
                .split([',', ';', '|'])
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(ToString::to_string)
                .collect()
        })
        .unwrap_or_default()
}

fn parse_link_by_icon(block: &str, icon: &str) -> Option<String> {
    let pattern = format!(
        r#"(?is)<a[^>]*>\s*<span[^>]+class="[^"]*{}[^"]*"[^>]*>.*?</span>(.*?)</a>"#,
        regex::escape(icon)
    );
    first_capture(block, &pattern).map(|value| clean_html(&value))
}

fn parse_info(block: &str) -> Option<String> {
    first_capture(
        block,
        r#"(?is)<div[^>]+class="[^"]*text-gray-800[^"]*"[^>]*>(.*?)</div>"#,
    )
    .map(|value| clean_html(&value))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_archive_books_html() {
        let html = r#"
        <html>
          <body>
            <div class="record-list-outer">
              <div>
                <a class="text-lg" href="/md5/abc123">The Rust Book</a>
                <a href="/search?q=Steve"><span class="user"></span>Steve Klabnik</a>
                <a href="/search?q=NoStarch"><span class="company"></span>No Starch</a>
                <div class="text-gray-800">English | 2019 | pdf | 552 pages | 10 MB</div>
                <img src="/covers/sample.jpg">
              </div>
            </div>
          </body>
        </html>
        "#;

        assert_eq!(
            parse_books_html(html),
            vec![BookResult {
                title: "The Rust Book".to_string(),
                url: "https://annas-archive.org/md5/abc123".to_string(),
                author: Some("Steve Klabnik".to_string()),
                authors: vec!["Steve Klabnik".to_string()],
                info: Some("English | 2019 | pdf | 552 pages | 10 MB".to_string()),
                year: Some("2019".to_string()),
                publisher: Some("No Starch".to_string()),
                extension: None,
                pages: None,
                size: None,
                language: None,
                thumbnail: Some("https://annas-archive.org/covers/sample.jpg".to_string()),
            }]
        );
    }
}
