pub mod config;
pub mod metadata;

use self::config::AccountConfig;
use self::metadata::FeverMetadata;
use crate::feed_api::{FeedApi, FeedApiError, FeedApiResult, FeedHeaderMap, Portal};
use crate::models::{
    self, ArticleID, Category, CategoryID, CategoryMapping, DirectLogin, FatArticle, FavIcon, Feed, FeedID, FeedMapping, FeedUpdateResult, Headline,
    LoginData, Marked, NEWSFLASH_TOPLEVEL, PasswordLogin, PluginCapabilities, Read, SyncResult, TagID, Url,
};
use crate::util;
use crate::util::favicons::EXPIRES_AFTER_DAYS;
use async_trait::async_trait;

use base64::Engine;
use base64::engine::general_purpose::STANDARD as base64_std;
use chrono::{Duration, Utc};
use fever_api::FeverApi;
use fever_api::error::ApiError as FeverApiError;
use fever_api::models::{Feed as FeverFeed, FeedsGroups, Group as FeverCategory, Item as FeverEntry, ItemStatus};
use futures::future;
use reqwest::Client;
use reqwest::header::{HeaderMap, HeaderValue};
use std::collections::HashMap;
use std::collections::HashSet;
use std::convert::TryInto;
use std::sync::Arc;

impl From<FeverApiError> for FeedApiError {
    fn from(error: FeverApiError) -> FeedApiError {
        match error {
            FeverApiError::Url(e) => FeedApiError::Url(e),
            FeverApiError::Json { source, json } => FeedApiError::Json { source, json },
            FeverApiError::Http(e) => FeedApiError::Network(e),
            FeverApiError::Fever(fever_error) => FeedApiError::Api {
                message: format!("Fever Error (code {})\nMessage: {}", fever_error.error_code, fever_error.error_message),
            },
            FeverApiError::Input => FeedApiError::Api {
                message: FeverApiError::Input.to_string(),
            },
            FeverApiError::Token => FeedApiError::Api {
                message: FeverApiError::Token.to_string(),
            },
            FeverApiError::Parse => FeedApiError::Api {
                message: FeverApiError::Parse.to_string(),
            },
            FeverApiError::Unauthorized => FeedApiError::Auth,
            FeverApiError::Unknown => FeedApiError::Unknown,
        }
    }
}

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

impl Fever {
    fn convert_category(category: FeverCategory, sort_index: Option<i32>) -> (Category, CategoryMapping) {
        let FeverCategory { id, title } = category;
        let category_id = CategoryID::new(&id.to_string());

        let category = Category {
            category_id: category_id.clone(),
            label: title,
        };
        let category_mapping = CategoryMapping {
            parent_id: NEWSFLASH_TOPLEVEL.clone(),
            category_id,
            sort_index,
        };

        (category, category_mapping)
    }

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

    fn convert_feed(feed: FeverFeed) -> Feed {
        let FeverFeed {
            id,
            favicon_id: _,
            title,
            url,
            site_url,
            is_spark: _,
            last_updated_on_time: _,
        } = feed;

        Feed {
            feed_id: FeedID::new(&id.to_string()),
            label: title,
            website: site_url.and_then(|url| Url::parse(&url).ok()),
            feed_url: Url::parse(&url).ok(),
            icon_url: None,
            error_count: 0,
            error_message: None,
        }
    }

    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)| {
                mappings.push(FeedMapping {
                    feed_id: FeedID::new(&f.id.to_string()),
                    category_id: group_mapping
                        .get(&f.id)
                        .map(|id| CategoryID::new(&id.to_string()))
                        .unwrap_or_else(|| NEWSFLASH_TOPLEVEL.clone()),
                    sort_index: Some(i as i32),
                });

                Self::convert_feed(f)
            })
            .collect();

        (feeds, mappings)
    }

    fn convert_entry(entry: FeverEntry, portal: Arc<Box<dyn Portal>>) -> FatArticle {
        let FeverEntry {
            id,
            feed_id,
            title,
            author,
            html,
            url,
            is_saved,
            is_read,
            created_on_time,
        } = entry;

        let article_id = ArticleID::new(&id.to_string());
        let article_exists_locally = portal.get_article_exists(&article_id).unwrap_or(false);
        let plain_text = if article_exists_locally {
            None
        } else {
            Some(util::html2text::html2text(&html))
        };
        let summary = plain_text.as_deref().map(util::html2text::text2summary);
        let thumbnail_url = crate::util::thumbnail::extract_thumbnail(&html);

        FatArticle {
            article_id,
            title: match escaper::decode_html(&title) {
                Ok(title) => Some(title),
                Err(error) => {
                    tracing::warn!(?error.kind, %error.position, "Failed to decode html");
                    Some(title)
                }
            },
            author: if author.is_empty() { None } else { Some(author) },
            feed_id: FeedID::new(&feed_id.to_string()),
            url: Url::parse(&url).ok(),
            date: util::timestamp_to_datetime(created_on_time),
            synced: Utc::now(),
            updated: None,
            summary,
            html: Some(html),
            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,
            thumbnail_url,
        }
    }

    fn convert_entry_vec(entries: Vec<FeverEntry>, feed_ids: &HashSet<FeedID>, portal: Arc<Box<dyn Portal>>) -> Vec<FatArticle> {
        entries
            .into_iter()
            .filter_map(|e| {
                let feed_ids = feed_ids.clone();
                let portal = portal.clone();

                let feed_id = FeedID::new(&e.feed_id.to_string());
                if feed_ids.contains(&feed_id) || e.is_saved {
                    Some(Self::convert_entry(e, portal))
                } else {
                    None
                }
            })
            .collect()
    }

    pub async fn get_articles(
        &self,
        api: &Arc<FeverApi>,
        item_ids: Vec<u64>,
        client: &Client,
        feeds: &[Feed],
    ) -> Result<Vec<FatArticle>, FeverApiError> {
        let batch_size: usize = 50;
        let mut tasks = Vec::new();
        let feed_ids: HashSet<FeedID> = feeds.iter().map(|f| f.feed_id.clone()).collect();
        let item_id_chunks = item_ids.chunks(batch_size);

        for chunk in item_id_chunks {
            let feed_ids = feed_ids.clone();
            let client = client.clone();
            let chunk = chunk.to_vec();
            let api = api.clone();
            let portal = self.portal.clone();

            let task = tokio::spawn(async move {
                let entries = api.get_items_with(chunk.to_vec(), &client).await?;
                let converted_articles = Self::convert_entry_vec(entries.items, &feed_ids, portal);
                Ok::<Vec<FatArticle>, FeedApiError>(converted_articles)
            });

            tasks.push(task);
        }

        let articles = future::join_all(tasks)
            .await
            .into_iter()
            .filter_map(|res| if let Ok(Ok(v)) = res { Some(v) } else { None })
            .flatten()
            .collect();
        Ok(articles)
    }

    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_reachable(&self, client: &Client) -> FeedApiResult<bool> {
        if let Some(url) = self.config.get_url() {
            let res = client.head(&url).send().await?;
            Ok(res.status().is_success())
        } else {
            Err(FeedApiError::Login)
        }
    }

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

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

    async fn get_login_data(&self) -> Option<LoginData> {
        if self.has_user_configured().unwrap_or(false) {
            let user = self.config.get_user_name();
            let password = self.config.get_password();

            if let (Some(user), Some(password)) = (user, password) {
                return Some(LoginData::Direct(DirectLogin::Password(PasswordLogin {
                    id: FeverMetadata::get_id(),
                    url: self.config.get_url(),
                    user,
                    password,
                    basic_auth: None, // fever authentication already uses multi-form
                })));
            }
        }

        None
    }

    async fn login(&mut self, data: LoginData, client: &Client) -> FeedApiResult<()> {
        if let LoginData::Direct(DirectLogin::Password(data)) = data
            && let Some(url_string) = data.url.clone()
        {
            let url = Url::parse(&url_string)?;
            let api = if let Some(basic_auth) = &data.basic_auth {
                FeverApi::new_with_http_auth(&url, &data.user, &data.password, &basic_auth.user, basic_auth.password.as_deref())
            } else {
                FeverApi::new(&url, &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.basic_auth.as_ref().map(|auth| auth.user.clone()).as_deref());
            self.config.set_http_password(data.basic_auth.and_then(|auth| auth.password).as_deref());
            self.config.write()?;
            let valid = api.valid_credentials(client).await?;
            if valid {
                self.api = Some(Arc::new(api));
                self.logged_in = true;
                return Ok(());
            }
        }

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

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

    async fn initial_sync(&self, client: &Client, _custom_header: FeedHeaderMap) -> FeedApiResult<SyncResult> {
        if let Some(api) = &self.api {
            let categories = api.get_groups(client);
            let feeds = api.get_feeds(client);

            // starred articles
            let starred_ids = api.get_saved_items(client);

            // unread articles
            let unread_ids = api.get_unread_items(client);

            // 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);

            let (categories, feeds, starred_ids, unread_ids, entries) = futures::try_join!(categories, feeds, starred_ids, unread_ids, entries)?;

            let (categories, category_mappings) = Self::convert_category_vec(categories.groups);
            let (feeds, feed_mappings) = Self::convert_feed_vec(feeds.feeds, feeds.feeds_groups);
            let feed_ids: HashSet<FeedID> = feeds.iter().map(|f| f.feed_id.clone()).collect();

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

            let mut starred = self.get_articles(api, starred_ids.saved_item_ids, client, &feeds).await?;
            articles.append(&mut starred);

            let mut unread = self.get_articles(api, unread_ids.unread_item_ids, client, &feeds).await?;
            articles.append(&mut unread);

            let mut read_articles = Self::convert_entry_vec(entries.items, &feed_ids, self.portal.clone());
            articles.append(&mut read_articles);

            return Ok(SyncResult {
                feeds: util::vec_to_option(feeds),
                categories: util::vec_to_option(categories),
                feed_mappings: util::vec_to_option(feed_mappings),
                category_mappings: util::vec_to_option(category_mappings),
                tags: None,
                taggings: None,
                headlines: None,
                articles: util::vec_to_option(articles),
                enclosures: None,
            });
        }
        Err(FeedApiError::Login)
    }

    async fn sync(&self, client: &Client, _custom_header: FeedHeaderMap) -> FeedApiResult<SyncResult> {
        if let Some(api) = &self.api {
            let categories = api.get_groups(client);
            let feeds = api.get_feeds(client);

            let fever_unread_ids = api.get_unread_items(client);
            let fever_marked_ids = api.get_saved_items(client);

            let (categories, feeds, fever_unread_ids, fever_marked_ids) = futures::try_join!(categories, feeds, fever_unread_ids, fever_marked_ids)?;

            let (categories, category_mappings) = Self::convert_category_vec(categories.groups);
            let (feeds, feed_mappings) = Self::convert_feed_vec(feeds.feeds, feeds.feeds_groups);
            let feed_ids: HashSet<FeedID> = feeds.iter().map(|f| f.feed_id.clone()).collect();

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

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

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

            // unread article ids
            let fever_unread_ids: HashSet<u64> = fever_unread_ids.unread_item_ids.into_iter().collect();

            // marked (saved/starred) article ids
            let fever_marked_ids: HashSet<u64> = fever_marked_ids.saved_item_ids.into_iter().collect();

            let missing_unread_ids = fever_unread_ids.difference(&local_unread_ids).cloned().collect();
            let missing_marked_ids = fever_marked_ids.difference(&local_marked_ids).cloned().collect();

            // sync new unread articles
            let missing_unread_articles = self.get_articles(api, missing_unread_ids, client, &feeds);

            // sync new marked articles
            let missing_marked_articles = self.get_articles(api, missing_marked_ids, client, &feeds);

            // latest articles
            let entries = api.get_items(client);

            let (mut missing_unread_articles, mut missing_marked_articles, latest_entries) =
                futures::try_join!(missing_unread_articles, missing_marked_articles, entries)?;

            let mut latest_articles = Self::convert_entry_vec(latest_entries.items, &feed_ids, self.portal.clone());

            articles.append(&mut missing_unread_articles);
            articles.append(&mut missing_marked_articles);
            articles.append(&mut latest_articles);

            // mark remotely read article as read
            let mut should_mark_read_headlines = local_unread_ids
                .difference(&fever_unread_ids)
                .cloned()
                .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);

            // 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 mut missing_unmarked_headlines = local_marked_ids
                .difference(&fever_marked_ids)
                .cloned()
                .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);

            return Ok(SyncResult {
                feeds: util::vec_to_option(feeds),
                categories: util::vec_to_option(categories),
                feed_mappings: util::vec_to_option(feed_mappings),
                category_mappings: util::vec_to_option(category_mappings),
                tags: None,
                taggings: None,
                headlines: util::vec_to_option(headlines),
                articles: util::vec_to_option(articles),
                enclosures: None,
            });
        }
        Err(FeedApiError::Login)
    }

    async fn fetch_feed(&self, _feed_id: &FeedID, _client: &Client, _custom_header: HeaderMap<HeaderValue>) -> FeedApiResult<FeedUpdateResult> {
        Err(FeedApiError::Unsupported)
    }

    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?;
            }

            return Ok(());
        }
        Err(FeedApiError::Login)
    }

    async fn set_article_marked(&self, articles: &[ArticleID], marked: Marked, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            for article in articles {
                if let Ok(entry_id) = article.as_str().parse::<i64>() {
                    match marked {
                        models::Marked::Marked => api.mark_item(ItemStatus::Saved, entry_id.try_into().unwrap(), client).await?,
                        models::Marked::Unmarked => api.mark_item(ItemStatus::Unsaved, entry_id.try_into().unwrap(), client).await?,
                    }
                };
            }

            return Ok(());
        }
        Err(FeedApiError::Login)
    }

    async fn set_feed_read(&self, feeds: &[FeedID], _articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp();

            for feed in feeds {
                let id = feed.to_string().parse::<i64>().unwrap();
                api.mark_feed(ItemStatus::Read, id, last_sync, client).await?;
            }
            return Ok(());
        }
        Err(FeedApiError::Login)
    }

    async fn set_category_read(&self, categories: &[CategoryID], _articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp();

            for category in categories {
                let id = category.to_string().parse::<i64>().unwrap();
                api.mark_group(ItemStatus::Read, id, last_sync, client).await?;
            }
            return Ok(());
        }
        Err(FeedApiError::Login)
    }

    async fn set_tag_read(&self, _tags: &[TagID], _articles: &[ArticleID], _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiError::Unsupported)
    }

    async fn set_all_read(&self, _articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let last_sync = self.portal.get_config().read().await.get_last_sync();
            api.mark_group(ItemStatus::Read, 0, last_sync.timestamp(), client).await?;
            return Ok(());
        }
        Err(FeedApiError::Login)
    }

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

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

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

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

    async fn edit_feed_url(&self, _feed_id: &FeedID, _new_url: &str, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiError::Unsupported)
    }

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

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

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

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

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

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

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

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

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

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

    async fn get_favicon(&self, feed_id: &FeedID, client: &Client, _custom_header: HeaderMap<HeaderValue>) -> FeedApiResult<FavIcon> {
        if let Some(api) = &self.api {
            let fever_feed_id = feed_id.to_string().parse::<u64>().map_err(|e| FeedApiError::Api {
                message: format!("Failed to parse feed id {e}"),
            })?;

            let feeds = api.get_feeds(client).await?;

            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?.favicons;

            if let Some(favicon) = favicon_set.get(&favicon_id)
                && let Some(start) = favicon.data.find(',')
            {
                let data = base64_std.decode(&favicon.data[start + 1..]).map_err(|_| FeedApiError::Encryption)?;

                let favicon = FavIcon {
                    feed_id: feed_id.clone(),
                    expires: Utc::now() + Duration::try_days(EXPIRES_AFTER_DAYS).unwrap(),
                    format: Some(favicon.mime_type.clone()),
                    etag: None,
                    source_url: None,
                    data: Some(data),
                };

                return Ok(favicon);
            }
        }

        Err(FeedApiError::Login)
    }
}
