pub mod config;
pub mod metadata;

use self::config::AccountConfig;
use self::metadata::FeverMetadata;
use crate::feed_api::{FeedApi, FeedApiError, FeedApiErrorKind, FeedApiResult, Portal};
use crate::models::{
    self, ArticleID, Category, CategoryID, CategoryType, Enclosure, FatArticle, FavIcon, Feed, FeedID, FeedMapping, Headline, LoginData, Marked,
    PasswordLogin, PluginCapabilities, Read, SyncResult, TagID, Url, NEWSFLASH_TOPLEVEL,
};
use crate::util;
use crate::util::favicon_cache::EXPIRES_AFTER_DAYS;
use crate::util::html2text::Html2Text;
use async_trait::async_trait;
use chrono::{DateTime, Duration, TimeZone, Utc};
use failure::{Context, ResultExt};
use fever_api::error::ApiErrorKind as FeverApiErrorKind;
use fever_api::models::{Feed as FeverFeed, FeedsGroups, Group as FeverCategory, Item as FeverEntry, ItemStatus};
use fever_api::FeverApi;
use log::warn;
use rayon::prelude::*;
use reqwest::Client;
use std::collections::HashMap;
use std::collections::HashSet;
use std::convert::TryInto;

pub struct Fever {
    api: Option<FeverApi>,
    portal: Box<dyn Portal>,
    logged_in: bool,
    config: AccountConfig,
}

impl Fever {
    fn convert_category_vec(mut categories: Vec<FeverCategory>) -> Vec<Category> {
        categories
            .drain(..)
            .enumerate()
            .map(|(i, c)| Self::convert_category(c, Some(i as i32)))
            .collect()
    }

    fn convert_category(category: FeverCategory, sort_index: Option<i32>) -> Category {
        let (id, title) = category.decompose();
        Category {
            category_id: CategoryID::new(&id.to_string()),
            label: title,
            sort_index,
            parent_id: NEWSFLASH_TOPLEVEL.clone(),
            category_type: CategoryType::Default,
        }
    }

    fn convert_feed(feed: FeverFeed, sort_index: Option<i32>) -> Feed {
        let (id, _favicon_id, title, feed_url, site_url, _is_spark, _last_updated_on_time) = feed.decompose();

        Feed {
            feed_id: FeedID::new(&id.to_string()),
            label: title,
            website: match Url::parse(&site_url) {
                Ok(url) => Some(url),
                Err(_) => None,
            },
            feed_url: match Url::parse(&feed_url) {
                Ok(url) => Some(url),
                Err(_) => None,
            },
            icon_url: None,
            sort_index,
        }
    }

    fn convert_feed_vec(mut feeds: Vec<FeverFeed>, feeds_groups: Vec<FeedsGroups>) -> (Vec<Feed>, Vec<FeedMapping>) {
        let mut group_mapping = HashMap::new();
        for group in feeds_groups {
            for feed_id in group.feed_ids {
                group_mapping.insert(feed_id, group.group_id);
            }
        }

        let mut mappings: Vec<FeedMapping> = Vec::new();
        let feeds = feeds
            .drain(..)
            .enumerate()
            .map(|(i, f)| {
                if let Some(category_id) = group_mapping.get(&f.id) {
                    mappings.push(FeedMapping {
                        feed_id: FeedID::new(&f.id.to_string()),
                        category_id: CategoryID::new(&category_id.to_string()),
                    });
                }

                Self::convert_feed(f, Some(i as i32))
            })
            .collect();

        (feeds, mappings)
    }

    fn convert_entry(entry: FeverEntry, portal: &Box<dyn Portal>) -> FatArticle {
        let (id, feed_id, title, author, content, url, is_saved, is_read, published_at) = entry.decompose();

        let article_id = ArticleID::new(&id.to_string());
        let article_exists_locally = match portal.get_article_exists(&article_id) {
            Ok(exists) => exists,
            Err(_) => false,
        };
        let plain_text = if article_exists_locally { None } else { Html2Text::process(&content) };
        let summary = if let Some(plain_text) = &plain_text {
            Some(Html2Text::to_summary(plain_text))
        } else {
            None
        };

        FatArticle {
            article_id,
            title: match escaper::decode_html(&title) {
                Ok(title) => Some(title),
                Err(error) => {
                    warn!("Error {:?} at character {}", error.kind, error.position);
                    Some(title)
                }
            },
            author: if &author == "" { None } else { Some(author) },
            feed_id: FeedID::new(&feed_id.to_string()),
            url: match Url::parse(&url) {
                Ok(url) => Some(url),
                Err(_) => None,
            },
            date: Utc.timestamp(published_at.try_into().unwrap(), 0).naive_utc(),
            summary,
            html: Some(content),
            scraped_content: None,
            direction: None,
            unread: if is_read { models::Read::Read } else { models::Read::Unread },
            marked: if is_saved { models::Marked::Marked } else { models::Marked::Unmarked },
            plain_text,
        }
    }

    fn convert_entry_vec(entries: Vec<FeverEntry>, portal: &Box<dyn Portal>) -> (Vec<FatArticle>, Vec<Enclosure>) {
        let enclosures: Vec<Enclosure> = Vec::new();
        let articles = entries.into_par_iter().map(|e| Self::convert_entry(e, portal)).collect();

        (articles, enclosures)
    }

    pub async fn get_articles(&self, item_ids: Vec<u64>, client: &Client) -> FeedApiResult<Vec<FatArticle>> {
        if let Some(api) = &self.api {
            let batch_size: usize = 50;
            let mut articles: Vec<FatArticle> = Vec::new();
            let mut iter = item_ids.chunks(batch_size);

            while let Some(slice) = iter.next() {
                let entries = api.get_items_with(slice.to_vec(), client).await.context(FeedApiErrorKind::Api)?;

                let (mut converted_articles, _) = Self::convert_entry_vec(entries.items, &self.portal);
                articles.append(&mut converted_articles);
            }
            return Ok(articles);
        }
        Err(FeedApiErrorKind::Login)?
    }

    fn ids_to_fever_ids(ids: &[ArticleID]) -> Vec<u64> {
        ids.iter().filter_map(|article_id| article_id.to_string().parse::<u64>().ok()).collect()
    }
}

#[async_trait]
impl FeedApi for Fever {
    fn features(&self) -> FeedApiResult<PluginCapabilities> {
        Ok(PluginCapabilities::SUPPORT_CATEGORIES)
    }

    fn has_user_configured(&self) -> FeedApiResult<bool> {
        Ok(self.api.is_some())
    }

    async fn is_logged_in(&self, _client: &Client) -> FeedApiResult<bool> {
        Ok(self.logged_in)
    }

    fn user_name(&self) -> Option<String> {
        self.config.get_user_name()
    }

    fn get_login_data(&self) -> Option<LoginData> {
        if let Ok(true) = self.has_user_configured() {
            if let Some(username) = self.config.get_user_name() {
                if let Some(password) = self.config.get_password() {
                    return Some(LoginData::Password(PasswordLogin {
                        id: FeverMetadata::get_id(),
                        url: self.config.get_url(),
                        user: username,
                        password: password,
                        http_user: None, // fever authentication already uses multi-form
                        http_password: None,
                    }));
                }
            }
        }

        None
    }

    async fn login(&mut self, data: LoginData, client: &Client) -> FeedApiResult<()> {
        if let LoginData::Password(data) = data {
            let url_string = data.url.clone().ok_or(FeedApiErrorKind::Url)?;
            let url = Url::parse(&url_string).context(FeedApiErrorKind::Url)?;
            let api = if let Some(http_user) = &data.http_user {
                FeverApi::new_with_http_auth(&url.get(), &data.user, &data.password, http_user, data.http_password.as_deref())
            } else {
                FeverApi::new(&url.get(), &data.user, &data.password)
            };
            self.config.set_url(&url_string);
            self.config.set_password(&data.password);
            self.config.set_user_name(&data.user);
            self.config.set_http_user_name(data.http_user.as_deref());
            self.config.set_http_password(data.http_password.as_deref());
            self.config.write()?;
            let valid = api.valid_credentials(client).await.map_err(|error| {
                if error.kind() == FeverApiErrorKind::Unauthorized {
                    FeedApiErrorKind::HTTPAuth.into()
                } else {
                    FeedApiError::from(Context::from(error).map(|_c| FeedApiErrorKind::Api))
                }
            })?;
            if valid {
                self.api = Some(api);
                self.logged_in = true;
                return Ok(());
            }
        }

        self.logged_in = false;
        self.api = None;
        Err(FeedApiErrorKind::Login)?
    }

    async fn logout(&mut self, _client: &Client) -> FeedApiResult<()> {
        self.config.delete()?;
        Ok(())
    }

    async fn initial_sync(&self, client: &Client) -> FeedApiResult<SyncResult> {
        if let Some(api) = &self.api {
            let categories = api.get_groups(client).await.context(FeedApiErrorKind::Api)?;
            let categories = Self::convert_category_vec(categories.groups);

            let feeds = api.get_feeds(client).await.context(FeedApiErrorKind::Api)?;
            let (feeds, mappings) = Self::convert_feed_vec(feeds.feeds, feeds.feeds_groups);

            let mut articles: Vec<FatArticle> = Vec::new();

            // starred articles
            let starred_ids = api.get_saved_items(client).await.context(FeedApiErrorKind::Api)?;
            let mut starred = self.get_articles(starred_ids.saved_item_ids, client).await?;
            articles.append(&mut starred);

            // unread articles
            let unread_ids = api.get_unread_items(client).await.context(FeedApiErrorKind::Api)?;
            let mut unread = self.get_articles(unread_ids.unread_item_ids, client).await?;
            articles.append(&mut unread);

            // latest read articles
            // NOTE: currently there is no existing function to get the latest articles
            //       instead we crawl some arbitrary articles
            let entries = api.get_items(client).await.context(FeedApiErrorKind::Api)?;
            let (mut read_articles, _) = Self::convert_entry_vec(entries.items, &self.portal);
            articles.append(&mut read_articles);

            return Ok(SyncResult {
                feeds: util::vec_to_option(feeds),
                categories: util::vec_to_option(categories),
                mappings: util::vec_to_option(mappings),
                tags: None,
                taggings: None,
                headlines: None,
                articles: util::vec_to_option(articles),
                enclosures: None,
            });
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn sync(&self, _max_count: u32, _last_sync: DateTime<Utc>, client: &Client) -> FeedApiResult<SyncResult> {
        if let Some(api) = &self.api {
            let categories = api.get_groups(client).await.context(FeedApiErrorKind::Api)?;
            let categories = Self::convert_category_vec(categories.groups);

            let feeds = api.get_feeds(client).await.context(FeedApiErrorKind::Api)?;
            let (feeds, mappings) = Self::convert_feed_vec(feeds.feeds, feeds.feeds_groups);

            let mut articles: Vec<FatArticle> = Vec::new();
            let mut headlines: Vec<Headline> = Vec::new();

            // unread article ids
            let fever_unread_ids = api.get_unread_items(client).await.context(FeedApiErrorKind::Api)?;
            let fever_unread_ids: HashSet<u64> = fever_unread_ids.unread_item_ids.into_iter().collect();

            // marked (saved/starred) article ids
            let fever_marked_ids = api.get_saved_items(client).await.context(FeedApiErrorKind::Api)?;
            let fever_marked_ids: HashSet<u64> = fever_marked_ids.saved_item_ids.into_iter().collect();

            // get local unread
            let local_unread_ids = self.portal.get_article_ids_unread_all().context(FeedApiErrorKind::Portal)?;
            let local_unread_ids = Self::ids_to_fever_ids(&local_unread_ids);
            let local_unread_ids = local_unread_ids.into_iter().collect();

            // sync new unread articles
            let missing_unread_ids = fever_unread_ids.difference(&local_unread_ids).cloned().collect();
            let mut missing_unread_articles = self.get_articles(missing_unread_ids, client).await?;
            articles.append(&mut missing_unread_articles);

            // mark remotely read article as read
            let should_mark_read_ids: Vec<u64> = local_unread_ids.difference(&fever_unread_ids).cloned().collect();
            let mut should_mark_read_headlines = should_mark_read_ids
                .into_iter()
                .map(|id| Headline {
                    article_id: ArticleID::new(&id.to_string()),
                    unread: Read::Read,
                    marked: if fever_marked_ids.contains(&id) {
                        Marked::Marked
                    } else {
                        Marked::Unmarked
                    },
                })
                .collect();
            headlines.append(&mut should_mark_read_headlines);

            // get local marked
            let local_marked_ids = self.portal.get_article_ids_marked_all().context(FeedApiErrorKind::Portal)?;
            let local_marked_ids = Self::ids_to_fever_ids(&local_marked_ids);
            let local_marked_ids = local_marked_ids.into_iter().collect();

            // sync new marked articles
            let missing_marked_ids = fever_marked_ids.difference(&local_marked_ids).cloned().collect();
            let mut missing_marked_articles = self.get_articles(missing_marked_ids, client).await?;
            articles.append(&mut missing_marked_articles);

            // mark remotly starred articles locally
            let mut mark_headlines = fever_marked_ids
                .iter()
                .map(|id| Headline {
                    article_id: ArticleID::new(&id.to_string()),
                    marked: Marked::Marked,
                    unread: if fever_unread_ids.contains(&id) { Read::Unread } else { Read::Read },
                })
                .collect();
            headlines.append(&mut mark_headlines);

            // unmark remotly unstarred articles locally
            let missing_unmarked_ids: Vec<u64> = local_marked_ids.difference(&fever_marked_ids).cloned().collect();
            let mut missing_unmarked_headlines = missing_unmarked_ids
                .into_iter()
                .map(|id| Headline {
                    article_id: ArticleID::new(&id.to_string()),
                    marked: Marked::Unmarked,
                    unread: if fever_unread_ids.contains(&id) { Read::Unread } else { Read::Read },
                })
                .collect();
            headlines.append(&mut missing_unmarked_headlines);

            // latest articles
            let entries = api.get_items(client).await.context(FeedApiErrorKind::Api)?;
            let (mut read_articles, _) = Self::convert_entry_vec(entries.items, &self.portal);
            articles.append(&mut read_articles);

            return Ok(SyncResult {
                feeds: util::vec_to_option(feeds),
                categories: util::vec_to_option(categories),
                mappings: util::vec_to_option(mappings),
                tags: None,
                taggings: None,
                headlines: util::vec_to_option(headlines),
                articles: util::vec_to_option(articles),
                enclosures: None,
            });
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn set_article_read(&self, articles: &[ArticleID], read: Read, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let entries = Self::ids_to_fever_ids(articles);
            let status = match read {
                models::Read::Read => ItemStatus::Read,
                models::Read::Unread => ItemStatus::Unread,
            };
            for entry in entries {
                api.mark_item(status, entry, client).await.context(FeedApiErrorKind::Api)?;
            }

            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn set_article_marked(&self, articles: &[ArticleID], marked: Marked, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let articles = self.portal.get_articles(articles).context(FeedApiErrorKind::Portal)?;

            for article in articles {
                if article.marked != marked {
                    if let Ok(entry_id) = article.article_id.to_string().parse::<i64>() {
                        match marked {
                            models::Marked::Marked => api
                                .mark_item(ItemStatus::Saved, entry_id.try_into().unwrap(), client)
                                .await
                                .context(FeedApiErrorKind::Api)?,
                            models::Marked::Unmarked => api
                                .mark_item(ItemStatus::Unsaved, entry_id.try_into().unwrap(), client)
                                .await
                                .context(FeedApiErrorKind::Api)?,
                        }
                    };
                }
            }

            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn set_feed_read(&self, feeds: &[FeedID], last_sync: DateTime<Utc>, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            for feed in feeds {
                let id = feed.to_string().parse::<i64>().unwrap();
                api.mark_feed(ItemStatus::Read, id, last_sync.timestamp(), &client)
                    .await
                    .context(FeedApiErrorKind::Api)?;
            }
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn set_category_read(&self, categories: &[CategoryID], last_sync: DateTime<Utc>, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            for category in categories {
                let id = category.to_string().parse::<i64>().unwrap();
                api.mark_group(ItemStatus::Read, id, last_sync.timestamp(), &client)
                    .await
                    .context(FeedApiErrorKind::Api)?;
            }
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn set_tag_read(&self, _tags: &[TagID], _last_sync: DateTime<Utc>, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn set_all_read(&self, last_sync: DateTime<Utc>, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            api.mark_group(ItemStatus::Read, 0, last_sync.timestamp(), &client)
                .await
                .context(FeedApiErrorKind::Api)?;
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn add_feed(
        &self,
        _url: &Url,
        _title: Option<String>,
        _category_id: Option<CategoryID>,
        _client: &Client,
    ) -> FeedApiResult<(Feed, Option<Category>)> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn remove_feed(&self, _id: &FeedID, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn move_feed(&self, _feed_id: &FeedID, _from: &CategoryID, _to: &CategoryID, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn rename_feed(&self, _feed_id: &FeedID, _new_title: &str, _client: &Client) -> FeedApiResult<FeedID> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn add_category(&self, _title: &str, _parent: Option<&CategoryID>, _client: &Client) -> FeedApiResult<CategoryID> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn remove_category(&self, _id: &CategoryID, _remove_children: bool, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn rename_category(&self, _id: &CategoryID, _new_title: &str, _client: &Client) -> FeedApiResult<CategoryID> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn move_category(&self, _id: &CategoryID, _parent: &CategoryID, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn import_opml(&self, _opml: &str, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn add_tag(&self, _title: &str, _client: &Client) -> FeedApiResult<TagID> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn remove_tag(&self, _id: &TagID, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn rename_tag(&self, _id: &TagID, _new_title: &str, _client: &Client) -> FeedApiResult<TagID> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn tag_article(&self, _article_id: &ArticleID, _tag_id: &TagID, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn untag_article(&self, _article_id: &ArticleID, _tag_id: &TagID, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn get_favicon(&self, feed_id: &FeedID, client: &Client) -> FeedApiResult<FavIcon> {
        if let Some(api) = &self.api {
            let fever_feed_id = feed_id.to_string().parse::<u64>().context(FeedApiErrorKind::Unknown)?;

            let feeds = api.get_feeds(client).await.context(FeedApiErrorKind::Api)?;

            let mut favicon_id = 0;

            for feed in feeds.feeds {
                if feed.id == fever_feed_id {
                    favicon_id = feed.favicon_id;
                }
            }

            let favicon_set = api.get_favicons(client).await.context(FeedApiErrorKind::Api)?.favicons;

            if let Some(start) = favicon_set[&favicon_id].data.find(',') {
                let data = base64::decode(&favicon_set[&favicon_id].data[start + 1..]).context(FeedApiErrorKind::Encryption)?;

                let favicon = FavIcon {
                    feed_id: feed_id.clone(),
                    expires: Utc::now().naive_utc() + Duration::days(EXPIRES_AFTER_DAYS),
                    format: Some(favicon_set[&favicon_id].mime_type.clone()),
                    etag: None,
                    source_url: None,
                    data: Some(data),
                };

                return Ok(favicon);
            }
        }
        Err(FeedApiErrorKind::Login)?
    }
}
