use crate::{Result, err};
use chrono::{Duration, NaiveDate, Utc};
use quick_xml::Reader;
use quick_xml::events::Event;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Paper {
    pub id: String,
    pub title: String,
    pub abstract_text: String,
    pub authors: Vec<String>,
    pub published_date: Option<String>,
    pub updated_date: Option<String>,
    pub doi: Option<String>,
    pub arxiv_id: Option<String>,
    pub url: String,
    pub categories: Vec<String>,
    pub extracted_methods: Vec<String>,
    pub source_provider: String,
}

#[derive(Debug, Clone, Default)]
pub struct PaperQuery {
    pub query: Option<String>,
    pub category: Option<String>,
    pub since: Option<String>,
    pub from: Option<NaiveDate>,
    pub to: Option<NaiveDate>,
}

pub fn query_string(args: &PaperQuery) -> Result<String> {
    let mut parts = Vec::new();
    if let Some(query) = &args.query {
        parts.push(format!("all:{}", query));
    }
    if let Some(category) = &args.category {
        parts.push(format!("cat:{}", category));
    }
    if parts.is_empty() {
        return Err(err("scan needs --query, --category, or both"));
    }
    let (from, to) = date_range(args)?;
    if let (Some(from), Some(to)) = (from, to) {
        parts.push(format!(
            "submittedDate:[{}0000 TO {}2359]",
            from.format("%Y%m%d"),
            to.format("%Y%m%d")
        ));
    }
    Ok(parts.join(" AND "))
}

pub fn date_range(args: &PaperQuery) -> Result<(Option<NaiveDate>, Option<NaiveDate>)> {
    if let Some(since) = &args.since {
        let days = since
            .strip_suffix('d')
            .ok_or_else(|| err("--since only supports Nd, e.g. 30d"))?
            .parse::<i64>()?;
        let today = Utc::now().date_naive();
        return Ok((Some(today - Duration::days(days)), Some(today)));
    }
    Ok((args.from, args.to))
}

pub fn search_url(query: &str, limit: usize) -> String {
    format!(
        "https://export.arxiv.org/api/query?search_query={}&start=0&max_results={}&sortBy=submittedDate&sortOrder=descending",
        urlencoding::encode(query),
        limit
    )
}

pub async fn search(query: &str, limit: usize) -> Result<Vec<Paper>> {
    let client = reqwest::Client::builder()
        .user_agent("paper-gap/0.1 (local on-demand CLI; contact: unknown)")
        .timeout(std::time::Duration::from_secs(20))
        .build()?;
    let response = client.get(search_url(query, limit)).send().await?;
    if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
        return Err(err(
            "arXiv rate limit reached; wait a few minutes and retry with a smaller --limit",
        ));
    }
    let body = response.error_for_status()?.text().await?;
    parse(&body)
}

pub fn parse(xml: &str) -> Result<Vec<Paper>> {
    let mut reader = Reader::from_str(xml);
    reader.config_mut().trim_text(true);
    let mut papers = Vec::new();
    let mut current: Option<Paper> = None;
    let mut field = String::new();
    let mut in_entry = false;
    let mut author_name = false;
    loop {
        match reader.read_event()? {
            Event::Start(e) => {
                let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
                if name == "entry" {
                    in_entry = true;
                    current = Some(Paper {
                        id: String::new(),
                        title: String::new(),
                        abstract_text: String::new(),
                        authors: Vec::new(),
                        published_date: None,
                        updated_date: None,
                        doi: None,
                        arxiv_id: None,
                        url: String::new(),
                        categories: Vec::new(),
                        extracted_methods: Vec::new(),
                        source_provider: "arxiv".into(),
                    });
                } else if in_entry && name == "category" {
                    if let Some(term) = e.attributes().flatten().find(|a| a.key.as_ref() == b"term")
                    {
                        if let Some(p) = &mut current {
                            p.categories
                                .push(String::from_utf8_lossy(&term.value).to_string());
                        }
                    }
                } else {
                    author_name = in_entry && name == "name";
                    field = name;
                }
            }
            Event::Empty(e) => {
                let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
                if in_entry && name == "category" {
                    if let Some(term) = e.attributes().flatten().find(|a| a.key.as_ref() == b"term")
                    {
                        if let Some(p) = &mut current {
                            p.categories
                                .push(String::from_utf8_lossy(&term.value).to_string());
                        }
                    }
                }
            }
            Event::Text(e) => {
                if let Some(p) = &mut current {
                    let text = e.unescape()?.to_string();
                    match field.as_str() {
                        "id" => {
                            p.id = text.clone();
                            p.url = text.clone();
                            p.arxiv_id = text.rsplit('/').next().map(|s| s.to_string());
                        }
                        "title" => p.title.push_str(clean(&text).as_str()),
                        "summary" => p.abstract_text.push_str(clean(&text).as_str()),
                        "published" => p.published_date = Some(text),
                        "updated" => p.updated_date = Some(text),
                        "doi" | "arxiv:doi" => p.doi = Some(text),
                        "name" if author_name => p.authors.push(text),
                        _ => {}
                    }
                }
            }
            Event::End(e) => {
                let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
                if name == "entry" {
                    if let Some(mut p) = current.take() {
                        p.extracted_methods =
                            extract_acronyms(&format!("{} {}", p.title, p.abstract_text));
                        papers.push(p);
                    }
                    in_entry = false;
                }
                field.clear();
                author_name = false;
            }
            Event::Eof => break,
            _ => {}
        }
    }
    Ok(papers)
}

pub fn clean(s: &str) -> String {
    s.split_whitespace().collect::<Vec<_>>().join(" ")
}

pub fn extract_acronyms(text: &str) -> Vec<String> {
    let mut out = Vec::new();
    for word in text.split(|c: char| !c.is_alphanumeric() && c != '-') {
        if word.len() > 1
            && word.len() <= 12
            && word.chars().any(|c| c.is_ascii_uppercase())
            && word
                .chars()
                .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '-')
            && !out.iter().any(|w| w == word)
        {
            out.push(word.to_string());
        }
    }
    out
}
