mod error;

pub use self::error::{FeedParserError, FeedParserErrorKind};
use crate::models::{Feed, FeedID, Url};
use failure::ResultExt;
use feed_rs::parser;
use libxml::{parser::Parser, xpath::Context};
use log::{debug, error, warn};
use reqwest::Client;
use std::str;

#[derive(Debug)]
pub enum ParsedUrl {
    SingleFeed(Feed),
    MultipleFeeds(Vec<(String, Url)>),
}

#[derive(Debug, PartialEq)]
enum ContentType {
    Rss,
    Atom,
    JsonFeed,
    Html,
}

pub async fn download_and_parse_feed(
    url: &Url,
    id: &FeedID,
    title: Option<String>,
    order_id: Option<i32>,
    client: &Client,
) -> Result<ParsedUrl, FeedParserError> {
    let result = client.get(url.get()).send().await.context(FeedParserErrorKind::Http)?;
    if !result.status().is_success() {
        error!("Downloading feed failed: {}", url);
        return Err(FeedParserErrorKind::Http)?;
    }

    let content_type = check_content_type(result.headers().get(reqwest::header::CONTENT_TYPE));

    let result_string = result
        .text()
        .await
        .map_err(|err| {
            error!("Reading response as string failed: {}", url);
            err
        })
        .context(FeedParserErrorKind::Http)?;

    if content_type == Some(ContentType::Html) {
        debug!("ContentType: Html -> trying to parse page for feed url");
        if let Ok(feed_vec) = parse_html(&result_string, &url) {
            let len = feed_vec.len();

            if len == 1 {
                if let Some((_title, url)) = feed_vec.get(0) {
                    let new_result = client.get(url.get()).send().await.context(FeedParserErrorKind::Http)?;
                    let new_result_string = new_result.text().await.context(FeedParserErrorKind::Http)?;
                    return Ok(ParsedUrl::SingleFeed(parse_feed(new_result_string, url, id, title, order_id)?));
                }
            } else if len > 1 {
                return Ok(ParsedUrl::MultipleFeeds(feed_vec));
            } else {
                return Err(FeedParserErrorKind::Html)?;
            }
        }
    }

    Ok(ParsedUrl::SingleFeed(parse_feed(result_string, url, id, title, order_id)?))
}

fn parse_html(html: &str, base_url: &Url) -> Result<Vec<(String, Url)>, FeedParserError> {
    let parser = Parser::default_html();
    let xpath = "//link[@rel='alternate']";
    if let Ok(doc) = parser.parse_string(html) {
        if let Ok(xpath_ctx) = Context::new(&doc) {
            if let Ok(xpath_result) = xpath_ctx.evaluate(xpath) {
                let xpath_result = xpath_result.get_nodes_as_vec();
                let mut result_vec: Vec<(String, Url)> = Vec::new();

                if xpath_result.is_empty() {
                    warn!("xpath didn't yield any results: {}", xpath);
                    return Err(FeedParserErrorKind::Html)?;
                }

                for node in xpath_result {
                    if let Some(url) = node.get_property("href") {
                        if url.starts_with("android-app") || url.starts_with("ios-app") {
                            continue;
                        }
                        debug!("Parsing Html yielded feed: {}", url);
                        let title = match node.get_property("title") {
                            Some(title) => title,
                            None => url.clone(),
                        };

                        let url = match Url::parse(&url) {
                            Ok(url) => url,
                            Err(_) => match base_url.get().join(&url) {
                                Ok(url) => Url::new(url),
                                Err(err) => {
                                    warn!("Failed to parse url: {} - {}", url, err);
                                    continue;
                                }
                            },
                        };

                        match node.get_property("type") {
                            None => continue,
                            Some(_type) => {
                                if !_type.contains("rss") && !_type.contains("atom") && !_type.contains("xml") {
                                    continue;
                                }
                            }
                        }

                        result_vec.push((title, url));
                    } else {
                        warn!("<link> tag is missin href property");
                    }
                }

                return Ok(result_vec);
            } else {
                warn!("xpath evaluation failed: {}", xpath);
            }
        }
    } else {
        warn!("Failed to parse HTML");
    }

    Err(FeedParserErrorKind::Html)?
}

fn check_content_type(header: Option<&reqwest::header::HeaderValue>) -> Option<ContentType> {
    if let Some(header) = header {
        if let Ok(header) = header.to_str() {
            let type_ = match header.find('/') {
                Some(end) => header[..end].to_string(),
                None => "unknown".to_string(),
            };
            let subtype = match header.find('/') {
                None => "unknown".to_string(),
                Some(start) => match header.find('+') {
                    None => match header.find(';') {
                        None => "unknown".to_string(),
                        Some(end) => header[start + 1..end].to_string(),
                    },
                    Some(end) => header[start + 1..end].to_string(),
                },
            };
            let suffix = match header.find('+') {
                None => None,
                Some(start) => Some(header[start..].to_string()),
            };

            if type_ == "text" && subtype == "html" && suffix == None {
                return Some(ContentType::Html);
            }

            if type_ == "application" && subtype == "atom" {
                return Some(ContentType::Atom);
            }

            if (type_ == "text" && subtype == "xml") || (type_ == "application" && subtype == "rss") {
                return Some(ContentType::Rss);
            }

            if type_ == "application" && subtype == "vnd.api" {
                if let Some(suffix) = suffix {
                    if suffix == "json" {
                        return Some(ContentType::JsonFeed);
                    }
                }
            }
        }
    }

    None
}

fn parse_feed(feed: String, url: &Url, id: &FeedID, title: Option<String>, sort_index: Option<i32>) -> Result<Feed, FeedParserError> {
    if let Ok(feed) = parser::parse(feed.as_bytes()) {
        let title = match title {
            Some(title) => title,
            None => match feed.title {
                Some(title) => title.content.clone(),
                None => "Unknown Feed".to_owned(),
            },
        };

        let mut website: Option<Url> = None;
        if !feed.links.is_empty() {
            // see if there is a link with rel='alternate' -> this is probably what we want
            if let Some(link) = feed.links.iter().find(|link| link.rel == Some("alternate".to_owned())) {
                if let Ok(url) = Url::parse(&link.href) {
                    website = Some(url);
                }
            }
            // otherwise just take the first link
            else if let Ok(url) = Url::parse(&feed.links[0].href) {
                website = Some(url);
            }
        }

        let icon_url = match feed.icon {
            Some(image) => match Url::parse(&image.uri) {
                Ok(url) => Some(url),
                Err(_) => None,
            },
            None => None,
        };

        let feed = Feed {
            feed_id: id.clone(),
            label: title,
            website,
            feed_url: Some(url.clone()),
            icon_url,
            sort_index,
        };

        return Ok(feed);
    }

    warn!("Neither atom nor rss parsing succeeded: {}", url);
    Err(FeedParserErrorKind::Feed)?
}

#[cfg(test)]
mod tests {
    use super::ParsedUrl;
    use crate::models::{FeedID, Url};
    use crate::util::feed_parser;
    use reqwest::Client;

    #[tokio::test(basic_scheduler)]
    pub async fn golem_atom() {
        let url_text = "https://rss.golem.de/rss.php?feed=ATOM1.0";
        let url = Url::parse(url_text).unwrap();
        let feed_id = FeedID::new(url_text);
        let feed = feed_parser::download_and_parse_feed(&url, &feed_id, None, None, &Client::new())
            .await
            .unwrap();

        let feed = match feed {
            ParsedUrl::SingleFeed(feed) => feed,
            ParsedUrl::MultipleFeeds(_) => panic!("Expected Single Feed"),
        };

        assert_eq!(feed.label, "Golem.de");
        assert_eq!(feed.icon_url.unwrap().to_string(), "https://www.golem.de/favicon.ico");
        assert_eq!(feed.website.unwrap().to_string(), "https://www.golem.de/");
    }

    #[tokio::test(basic_scheduler)]
    pub async fn golem_rss() {
        let url_text = "https://rss.golem.de/rss.php?feed=RSS1.0";
        let url = Url::parse(url_text).unwrap();
        let feed_id = FeedID::new(url_text);
        let feed = feed_parser::download_and_parse_feed(&url, &feed_id, None, None, &Client::new())
            .await
            .unwrap();

        let feed = match feed {
            ParsedUrl::SingleFeed(feed) => feed,
            ParsedUrl::MultipleFeeds(_) => panic!("Expected Single Feed"),
        };

        assert_eq!(feed.label, "Golem.de");
        assert_eq!(feed.website.unwrap().to_string(), "https://www.golem.de/");
    }

    #[tokio::test(basic_scheduler)]
    pub async fn planet_gnome_rss() {
        let url_text = "http://planet.gnome.org/rss20.xml";
        let url = Url::parse(url_text).unwrap();
        let feed_id = FeedID::new(url_text);
        let feed = feed_parser::download_and_parse_feed(&url, &feed_id, None, None, &Client::new())
            .await
            .unwrap();

        let feed = match feed {
            ParsedUrl::SingleFeed(feed) => feed,
            ParsedUrl::MultipleFeeds(_) => panic!("Expected Single Feed"),
        };

        assert_eq!(feed.label, "Planet GNOME");
        assert_eq!(feed.icon_url, None);
        assert_eq!(feed.website.unwrap().to_string(), "https://planet.gnome.org/");
    }

    #[tokio::test(basic_scheduler)]
    pub async fn golem_find_feeds() {
        let url_text = "https://www.golem.de/";
        let url = Url::parse(url_text).unwrap();
        let feed_id = FeedID::new(url_text);
        let feed_vec = feed_parser::download_and_parse_feed(&url, &feed_id, None, None, &Client::new())
            .await
            .unwrap();

        let feed_vec = match feed_vec {
            ParsedUrl::MultipleFeeds(feed_vec) => feed_vec,
            ParsedUrl::SingleFeed(_) => panic!("Expected Multiple Feeds"),
        };

        assert_eq!(feed_vec.len(), 2);
        assert_eq!(feed_vec.get(0).unwrap().0, "Golem.de RSS Feed");
        assert_eq!(feed_vec.get(0).unwrap().1.get().as_str(), "https://rss.golem.de/rss.php?feed=RSS1.0");
        assert_eq!(feed_vec.get(1).unwrap().0, "Golem.de ATOM Feed");
        assert_eq!(feed_vec.get(1).unwrap().1.get().as_str(), "https://rss.golem.de/rss.php?feed=ATOM1.0");
    }

    #[tokio::test(basic_scheduler)]
    pub async fn theverge_find_feeds() {
        let url_text = "https://www.theverge.com/";
        let url = Url::parse(url_text).unwrap();
        let feed_id = FeedID::new(url_text);
        let feed_vec = feed_parser::download_and_parse_feed(&url, &feed_id, None, None, &Client::new())
            .await
            .unwrap();

        let feed_vec = match feed_vec {
            ParsedUrl::MultipleFeeds(feed_vec) => feed_vec,
            ParsedUrl::SingleFeed(_) => panic!("Expected Multiple Feeds"),
        };

        assert_eq!(feed_vec.len(), 2);
        assert_eq!(feed_vec.get(0).unwrap().0, "The Verge");
        assert_eq!(feed_vec.get(0).unwrap().1.get().as_str(), "https://www.theverge.com/rss/index.xml");
        assert_eq!(feed_vec.get(1).unwrap().0, "Front Page");
        assert_eq!(
            feed_vec.get(1).unwrap().1.get().as_str(),
            "https://www.theverge.com/rss/front-page/index.xml"
        );
    }
}
