mod error;
mod scraper;

use self::error::{FavIconError, FavIconErrorKind};
use self::scraper::IconScraper;
use crate::database::Database;
use crate::feed_api::FeedApi;
use crate::models::{FavIcon, Feed, FeedID, Url};
use chrono::{DateTime, Duration, Utc};
use failure::ResultExt;
use log::{error, info, warn};
use parking_lot::RwLock;
use reqwest::{self, Client};
use std::collections::hash_map::HashMap;
use std::sync::Arc;

pub static EXPIRES_AFTER_DAYS: i64 = 30;

pub struct FavIconCache {
    map: RwLock<HashMap<FeedID, FavIcon>>,
    db: RwLock<Arc<Database>>,
}

impl FavIconCache {
    pub fn new(db: &Arc<Database>) -> Result<Self, FavIconError> {
        let hash_map: RwLock<HashMap<FeedID, FavIcon>> = RwLock::new(HashMap::new());

        // fill cache with icons from db
        if let Ok(favicons) = db.read_favicons() {
            for favicon in favicons {
                hash_map.write().insert(favicon.feed_id.clone(), favicon);
            }
        }

        Ok(FavIconCache {
            map: hash_map,
            db: RwLock::new(db.clone()),
        })
    }

    pub async fn get_icon(&self, feed: &Feed, api: &RwLock<Box<dyn FeedApi>>, client: &Client) -> Result<FavIcon, FavIconError> {
        let favicon = match self.map.read().get(&feed.feed_id) {
            Some(favicon) => Some(favicon.clone()),
            None => None,
        };

        if let Some(favicon) = favicon {
            // if expired: remove from cache and move on
            if favicon.is_expired() {
                info!("Favicon for '{}' is expired", feed.label);
                self.map.write().remove(&feed.feed_id);
            } else {
                return Ok(favicon);
            }
        }

        // Check db for existing icon
        let favicon = self.db.read().read_favicon(&feed.feed_id);
        if let Ok(favicon) = favicon {
            if favicon.data.is_some() && !favicon.is_expired() {
                self.map.write().insert(feed.feed_id.clone(), favicon.clone());
                return Ok(favicon);
            } else {
                let favicon = self.get_new_icon(api, feed, Some(favicon), client).await?;
                return Ok(favicon);
            }
        } else {
            let favicon = self.get_new_icon(api, feed, None, client).await?;
            return Ok(favicon);
        }
    }

    pub async fn get_new_icon(
        &self,
        api: &RwLock<Box<dyn FeedApi>>,
        feed: &Feed,
        old_icon: Option<FavIcon>,
        client: &Client,
    ) -> Result<FavIcon, FavIconError> {
        info!("Downloading new icon for '{}'", feed.label);

        // try to download new icon
        let favicon = Self::fetch_new_icon(Some(api), feed, client, old_icon).await;

        // write new icon to db
        self.db
            .write()
            .insert_favicon(&favicon)
            .map_err(|e| {
                error!("Failed to write favicon to db '{}'", feed.label);
                e
            })
            .context(FavIconErrorKind::DB)?;

        // insert new icon into cache
        self.map.write().insert(feed.feed_id.clone(), favicon.clone());

        Ok(favicon)
    }

    pub async fn fetch_new_icon(api: Option<&RwLock<Box<dyn FeedApi>>>, feed: &Feed, client: &Client, old_icon: Option<FavIcon>) -> FavIcon {
        if let Some(api) = api {
            if let Ok(favicon) = api.read().get_favicon(&feed.feed_id, client).await {
                info!("Favicon downloaded from backend.");
                return favicon;
            }
        }

        if let Some(favicon) = Self::scrap(feed, client).await {
            info!("Favicon scraped from website.");
            return favicon;
        }

        if let Some(icon_url) = &feed.icon_url {
            if let Ok(favicon) = Self::download(icon_url, feed, client, old_icon).await {
                info!("Favicon downloaded from feed data.");
                return favicon;
            }
        }

        // everything failed -> return empty icon
        FavIcon {
            feed_id: feed.feed_id.clone(),
            expires: Self::gen_expires().naive_utc(),
            format: None,
            etag: None,
            source_url: None,
            data: None,
        }
    }

    pub async fn scrap(feed: &Feed, client: &Client) -> Option<FavIcon> {
        if let Some(website) = &feed.website {
            if let Ok(scraper) = IconScraper::from_http(website, client).await {
                let icon = scraper.fetch_best(client).await;
                if let Some((mime, url, data)) = icon {
                    info!("Scraped favicon from url: '{}'", url);
                    return Some(FavIcon {
                        feed_id: feed.feed_id.clone(),
                        expires: Self::gen_expires().naive_utc(),
                        format: mime,
                        etag: None,
                        source_url: Some(url),
                        data: Some(data),
                    });
                } else {
                    warn!("Failed to download best scrapped icon from: '{}'", website);
                }
            } else {
                warn!("Failed to scrap icon from: {}", website);
            }
        }

        None
    }

    async fn download(url: &Url, feed: &Feed, client: &Client, old_icon: Option<FavIcon>) -> Result<FavIcon, FavIconError> {
        if let Some(old_icon) = old_icon {
            if let Some(etag) = &old_icon.etag {
                let res = client.head(url.get()).send().await.context(FavIconErrorKind::Http)?;

                if let Some(http_etag) = res.headers().get(reqwest::header::ETAG) {
                    if let Ok(http_etag) = http_etag.to_str() {
                        if etag == http_etag {
                            return Ok(old_icon.clone());
                        }
                    }
                }
            }
        }

        let res = client.get(url.get()).send().await.context(FavIconErrorKind::Http)?;
        let mut etag: Option<String> = None;
        let mut content_type: Option<String> = None;
        if let Some(http_etag) = res.headers().get(reqwest::header::ETAG) {
            if let Ok(http_etag) = http_etag.to_str() {
                etag = Some(http_etag.to_owned());
            }
        }
        if let Some(http_content_type) = res.headers().get(reqwest::header::CONTENT_TYPE) {
            if let Ok(http_content_type) = http_content_type.to_str() {
                content_type = Some(http_content_type.to_owned());
            }
        }

        let data = res.bytes().await.context(FavIconErrorKind::Unknown)?.to_vec();

        Ok(FavIcon {
            feed_id: feed.feed_id.clone(),
            expires: Self::gen_expires().naive_utc(),
            format: content_type,
            etag,
            source_url: None,
            data: Some(data),
        })
    }

    fn gen_expires() -> DateTime<Utc> {
        Utc::now() + Duration::days(EXPIRES_AFTER_DAYS)
    }
}

#[cfg(test)]
mod tests {

    use super::FavIconCache;
    use crate::models::{Feed, FeedID, Url};
    use crate::util::feed_parser::{self, ParsedUrl};
    use reqwest::{Client, ClientBuilder};

    async fn prepare_feed(url_str: &str) -> (Client, Feed) {
        let client = Client::new();
        let url = Url::parse(url_str).unwrap();
        let feed_id = FeedID::new(url_str);
        let feed = feed_parser::download_and_parse_feed(&url, &feed_id, None, None, &client).await.unwrap();

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

        (client, feed)
    }

    #[tokio::test(basic_scheduler)]
    pub async fn golem() {
        let (client, golem_feed) = prepare_feed("https://rss.golem.de/rss.php?feed=ATOM1.0").await;
        let favicon = FavIconCache::fetch_new_icon(None, &golem_feed, &client, None).await;

        assert_eq!(favicon.feed_id, golem_feed.feed_id);
        assert!(favicon.format.expect("No favicon format").starts_with("image/"));
    }

    #[tokio::test(basic_scheduler)]
    pub async fn planet_gnome() {
        let (client, gnome_feed) = prepare_feed("http://planet.gnome.org/rss20.xml").await;
        let favicon = FavIconCache::fetch_new_icon(None, &gnome_feed, &client, None).await;

        assert_eq!(favicon.feed_id, gnome_feed.feed_id);
        assert!(favicon.format.expect("No favicon format").starts_with("image/"));
    }

    #[tokio::test(basic_scheduler)]
    pub async fn reddit_scraper() {
        let reddit_feed = Feed {
            feed_id: FeedID::new("http://reddit.com"),
            label: String::from("reddit"),
            website: Some(Url::parse("http://reddit.com").unwrap()),
            feed_url: None,
            icon_url: None,
            sort_index: None,
        };
        let client = Client::new();

        let favicon = FavIconCache::fetch_new_icon(None, &reddit_feed, &client, None).await;

        assert_eq!(favicon.feed_id, reddit_feed.feed_id);
        assert!(favicon.format.expect("No favicon format").starts_with("image/"));
    }

    #[tokio::test(basic_scheduler)]
    pub async fn golem_scraper() {
        let reddit_feed = Feed {
            feed_id: FeedID::new("http://golem.de"),
            label: String::from("Golem"),
            website: Some(Url::parse("http://golem.de").unwrap()),
            feed_url: None,
            icon_url: None,
            sort_index: None,
        };
        let client = Client::new();

        let favicon = FavIconCache::fetch_new_icon(None, &reddit_feed, &client, None).await;

        assert_eq!(favicon.feed_id, reddit_feed.feed_id);
        assert!(favicon.format.expect("No favicon format").starts_with("image/"));
    }

    #[tokio::test(basic_scheduler)]
    pub async fn serienjunkies_scraper() {
        let feed = Feed {
            feed_id: FeedID::new("https://www.serienjunkies.de/news/"),
            label: String::from("Serienjunkies"),
            website: Some(Url::parse("https://www.serienjunkies.de/news/").unwrap()),
            feed_url: None,
            icon_url: None,
            sort_index: None,
        };
        let client = ClientBuilder::new().user_agent("Wget/1.20.3 (linux-gnu)").build().unwrap();

        let favicon = FavIconCache::fetch_new_icon(None, &feed, &client, None).await;

        assert_eq!(favicon.feed_id, feed.feed_id);
        assert!(favicon.data.is_some());
    }

    #[tokio::test(basic_scheduler)]
    pub async fn spiegel_scraper() {
        let feed = Feed {
            feed_id: FeedID::new("http://www.spiegel.de/"),
            label: String::from("Serienjunkies"),
            website: Some(Url::parse("http://www.spiegel.de/").unwrap()),
            feed_url: None,
            icon_url: None,
            sort_index: None,
        };
        let client = ClientBuilder::new().user_agent("Wget/1.20.3 (linux-gnu)").build().unwrap();

        let favicon = FavIconCache::fetch_new_icon(None, &feed, &client, None).await;

        assert_eq!(favicon.feed_id, feed.feed_id);
        assert!(favicon.data.is_some());
    }
}
