pub mod config;
pub mod metadata;
mod feedly_secrets;

use self::config::AccountConfig;
use self::metadata::FeedlyMetadata;
use self::feedly_secrets::FeedlySecrets;
use crate::feed_api::{FeedApi, FeedApiErrorKind, FeedApiResult, Portal};
use crate::models;
use crate::models::{
    ArticleID, Category, CategoryID, CategoryType, Direction, Enclosure, EnclosureType, FatArticle, FavIcon, Feed, FeedID, FeedMapping, LoginData,
    OAuthData, PluginCapabilities, SyncResult, Tag, TagID, Tagging, Url, NEWSFLASH_TOPLEVEL,
};
use crate::util;
use crate::util::feed_parser::{self, ParsedUrl};
use crate::util::html2text::Html2Text;
use async_trait::async_trait;
use chrono::{DateTime, Duration, TimeZone, Utc};
use failure::ResultExt;
use feedly_api::models::{Category as FeedlyCategory, Content as FeedlyContent, Entry, Link, Subscription, SubscriptionInput, Tag as FeedlyTag};
use feedly_api::{ApiErrorKind, FeedlyApi};
use parking_lot::RwLock;
use rayon::prelude::*;
use regex::Regex;
use reqwest::Client;
use std::sync::Arc;
use std::collections::HashSet;

macro_rules! api_call {
    ( $sel:ident, $api:ident, $func:expr, $client:expr ) => {{
        let result = $func;
        if let Err(error) = &result {
            if error.kind() == ApiErrorKind::TokenExpired {
                let response = $api.refresh_auth_token($client).await.context(FeedApiErrorKind::Api)?;
                let token_expires = Utc::now() + Duration::seconds(i64::from(response.expires_in));
                $sel.config.write().set_access_token(&response.access_token);
                $sel.config.write().set_token_expires(&token_expires.timestamp().to_string());
                $sel.config.write().write().context(FeedApiErrorKind::Config)?;
                $func.context(FeedApiErrorKind::Api)?;
            }
        }
        result.context(FeedApiErrorKind::Api)?
    }};
}

pub struct Feedly {
    api: Option<FeedlyApi>,
    portal: Box<dyn Portal>,
    logged_in: bool,
    config: Arc<RwLock<AccountConfig>>,
}

impl Feedly {
    fn convert_category_vec(mut categories: Vec<FeedlyCategory>) -> Vec<Category> {
        categories
            .drain(..)
            .enumerate()
            .map(|(i, c)| {
                let (id, label, _description) = c.decompose();
                Category {
                    category_id: CategoryID::new(&id),
                    label: {
                        let mut category_label = "Unknown".to_string();
                        if let Some(l) = label {
                            category_label = l;
                        } else if let Ok(regex) = Regex::new(r#"(?<=category\/).*$"#) {
                            if let Some(captures) = regex.captures(&id) {
                                if let Some(regex_match) = captures.get(1) {
                                    category_label = regex_match.as_str().to_owned();
                                }
                            }
                        }
                        category_label
                    },
                    sort_index: Some(i as i32),
                    parent_id: NEWSFLASH_TOPLEVEL.clone(),
                    category_type: CategoryType::Default,
                }
            })
            .collect()
    }

    fn convert_tag_vec(mut tags: Vec<FeedlyTag>) -> Vec<Tag> {
        tags.drain(..)
            .enumerate()
            .filter_map(|(i, t)| {
                let (id, label, _description) = t.decompose();
                if id.contains("global") {
                    return None;
                }
                Some(Tag {
                    tag_id: TagID::new(&id),
                    color: None,
                    label: match label {
                        Some(label) => label,
                        None => {
                            let mut tag_label = "Unknown".to_string();
                            if let Some(l) = label {
                                tag_label = l;
                            } else if let Ok(regex) = Regex::new(r#"(?<=tag\/).*$"#) {
                                if let Some(captures) = regex.captures(&id) {
                                    if let Some(regex_match) = captures.get(1) {
                                        tag_label = regex_match.as_str().to_owned();
                                    }
                                }
                            }
                            tag_label
                        }
                    },
                    sort_index: Some(i as i32),
                })
            })
            .collect()
    }

    fn convert_subscription_vec(mut subscriptions: Vec<Subscription>) -> (Vec<Feed>, Vec<FeedMapping>) {
        let mut mappings: Vec<FeedMapping> = Vec::new();
        let feeds = subscriptions
            .drain(..)
            .enumerate()
            .map(|(i, f)| {
                let (
                    feed_id,
                    title,
                    categories,
                    website,
                    _updated,
                    _subscribers,
                    _velocity,
                    _topics,
                    _content_type,
                    icon_url,
                    _partial,
                    _sort_id,
                    _added,
                    visual_url,
                ) = f.decompose();

                if let Some(mut categories) = categories {
                    let mut feed_mappings = categories
                        .drain(..)
                        .map(|c| {
                            let (category_id, _, _) = c.decompose();
                            FeedMapping {
                                feed_id: FeedID::new(&feed_id),
                                category_id: CategoryID::new(&category_id),
                            }
                        })
                        .collect::<Vec<FeedMapping>>();
                    mappings.append(&mut feed_mappings);
                }

                Feed {
                    feed_id: FeedID::new(&feed_id),
                    label: title,
                    website: match website {
                        Some(url) => match Url::parse(&url) {
                            Ok(url) => Some(url),
                            Err(_) => None,
                        },
                        None => None,
                    },
                    feed_url: None,
                    icon_url: match icon_url {
                        Some(url) => match Url::parse(&url) {
                            Ok(url) => Some(url),
                            Err(_) => None,
                        },
                        None => match visual_url {
                            Some(url) => match Url::parse(&url) {
                                Ok(url) => Some(url),
                                Err(_) => None,
                            },
                            None => None,
                        },
                    },
                    sort_index: Some(i as i32),
                }
            })
            .collect();

        (feeds, mappings)
    }

    fn convert_entry_vec(
        entries: Vec<Entry>,
        marked_tag: &str,
        feed_ids: &HashSet<FeedID>,
        portal: &Box<dyn Portal>
    ) -> (Vec<FatArticle>, Vec<Enclosure>, Vec<Tagging>) {
        let enclosures: RwLock<Vec<Enclosure>> = RwLock::new(Vec::new());
        let taggings: RwLock<Vec<Tagging>> = RwLock::new(Vec::new());
        let articles = entries
            .into_par_iter()
            .filter_map(|e| {
                let (
                    id,
                    title,
                    content,
                    summary,
                    author,
                    crawled,
                    _recrawled,
                    _published,
                    _updated,
                    alternate,
                    origin,
                    _keywords,
                    _visual,
                    unread,
                    tags,
                    _categories,
                    _engagement,
                    _action_timestamp,
                    enclosure,
                    _fingerprint,
                    _origin_id,
                    _sid,
                ) = e.decompose();

                let feed_id = match origin {
                    Some(origin) => match origin.stream_id {
                        Some(stream_id) => FeedID::new(&stream_id),
                        None => FeedID::new("None"),
                    },
                    None => FeedID::new("None"),
                };

                if !feed_ids.contains(&feed_id) {
                    return None;
                }

                if let Some(ref mut article_enclosures) = Feedly::convert_enclosures(&enclosure, ArticleID::new(&id)) {
                    enclosures.write().append(article_enclosures);
                }

                if let Some(tag_vec) = &tags {
                    let mut article_taggings: Vec<Tagging> = tag_vec
                        .iter()
                        .filter(|t| !t.id.contains("global."))
                        .map(|t| Tagging {
                            article_id: ArticleID::new(&id),
                            tag_id: TagID::new(&t.id),
                        })
                        .collect();
                    taggings.write().append(&mut article_taggings);
                }

                let (html, direction) = match Feedly::convert_content(&content) {
                    Some((html, direction)) => (Some(html), Some(direction)),
                    None => match Feedly::convert_content(&summary) {
                        Some((html, direction)) => (Some(html), Some(direction)),
                        None => (None, None),
                    },
                };

                let article_id = ArticleID::new(&id);
                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 {
                    if let Some(html) = &html {
                        Html2Text::process(html)
                    } else {
                        None
                    }
                };
                let summary = if let Some(plain_text) = &plain_text {
                    Some(Html2Text::to_summary(plain_text))
                } else {
                    None
                };

                Some(FatArticle {
                    article_id,
                    title,
                    author,
                    feed_id,
                    url: match alternate {
                        Some(alternates) => match alternates.first() {
                            Some(link_obj) => match Url::parse(&link_obj.href) {
                                Ok(url) => Some(url),
                                Err(_) => None,
                            },
                            None => None,
                        },
                        None => None,
                    },
                    date: Utc.timestamp((crawled / 1000) as i64, 0).naive_utc(),
                    html,
                    summary,
                    direction,
                    unread: if unread { models::Read::Unread } else { models::Read::Read },
                    marked: match tags {
                        Some(ref tags) => match tags.iter().find(|ref t| t.id.contains(marked_tag)) {
                            Some(_) => models::Marked::Marked,
                            None => models::Marked::Unmarked,
                        },
                        None => models::Marked::Unmarked,
                    },
                    scraped_content: None,
                    plain_text,
                })
            })
            .collect();

        (articles, enclosures.into_inner(), taggings.into_inner())
    }

    fn convert_content(content: &Option<FeedlyContent>) -> Option<(String, Direction)> {
        match content {
            Some(ref c) => {
                let direction = match c.direction {
                    Some(ref direction) => {
                        if direction == "rtl" {
                            Direction::RightToLeft
                        } else {
                            Direction::LeftToRight
                        }
                    }
                    None => Direction::LeftToRight,
                };

                Some((c.content.clone(), direction))
            }
            None => None,
        }
    }

    fn convert_enclosures(enclosures: &Option<Vec<Link>>, article_id: ArticleID) -> Option<Vec<Enclosure>> {
        match enclosures {
            Some(ref enclosure_vec) => {
                let res = enclosure_vec
                    .iter()
                    .map(|enc| Feedly::convert_enclosure(enc, &article_id))
                    .collect::<Result<Vec<Enclosure>, _>>();
                match res {
                    Ok(res) => Some(res),
                    Err(_) => None,
                }
            }
            None => None,
        }
    }

    fn convert_enclosure(enc: &Link, article_id: &ArticleID) -> FeedApiResult<Enclosure> {
        let url = Url::parse(&enc.href).context(FeedApiErrorKind::Url)?;
        Ok(Enclosure {
            article_id: article_id.clone(),
            url,
            enclosure_type: match enc._type {
                Some(ref type_string) => EnclosureType::from_string(&type_string),
                None => EnclosureType::File,
            },
        })
    }

    async fn get_articles(
        &self,
        stream_id: &str,
        count: Option<u32>,
        ranked: Option<&str>,
        unread_only: Option<bool>,
        newer_than: Option<u64>,
        feed_ids: &HashSet<FeedID>,
        client: &Client,
    ) -> FeedApiResult<(Vec<FatArticle>, Vec<Enclosure>, Vec<Tagging>)> {
        if let Some(api) = &self.api {
            let mut continuation: Option<String> = None;
            let mut articles: Vec<FatArticle> = Vec::new();
            let mut enclosures: Vec<Enclosure> = Vec::new();
            let mut taggings: Vec<Tagging> = Vec::new();
            let tag_marked = api.tag_marked(client).await.context(FeedApiErrorKind::Api)?;

            loop {
                let stream = api
                    .get_stream(stream_id, continuation, count, ranked, unread_only, newer_than, client)
                    .await
                    .context(FeedApiErrorKind::Api)?;
                let (mut converted_articles, mut converted_enclosures, mut converted_taggings) =
                    Feedly::convert_entry_vec(stream.items, &tag_marked, feed_ids, &self.portal);
                articles.append(&mut converted_articles);
                enclosures.append(&mut converted_enclosures);
                taggings.append(&mut converted_taggings);
                continuation = stream.continuation;

                if continuation.is_none() {
                    break;
                }
            }

            return Ok((articles, enclosures, taggings));
        }
        Err(FeedApiErrorKind::Login)?
    }
}

#[async_trait]
impl FeedApi for Feedly {
    fn features(&self) -> FeedApiResult<PluginCapabilities> {
        Ok(PluginCapabilities::ADD_REMOVE_FEEDS
            | PluginCapabilities::SUPPORT_CATEGORIES
            | PluginCapabilities::MODIFY_CATEGORIES
            | PluginCapabilities::SUPPORT_TAGS)
    }

    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.read().get_user_name()
    }

    fn get_login_data(&self) -> Option<LoginData> {
        if let Ok(true) = self.has_user_configured() {
            return Some(LoginData::OAuth(OAuthData {
                id: FeedlyMetadata::get_id(),
                url: String::new(),
            }));
        }

        None
    }

    async fn login(&mut self, data: LoginData, client: &Client) -> FeedApiResult<()> {
        if let LoginData::OAuth(data) = data {
            let url = Url::parse(&data.url).context(FeedApiErrorKind::Url)?;
            let secret_struct = FeedlySecrets::new();
            match FeedlyApi::parse_redirected_url(url.get()) {
                Ok(auth_code) => match FeedlyApi::request_auth_token(&secret_struct.client_id, &secret_struct.client_secret, auth_code, client).await
                {
                    Ok(response) => {
                        let now = Utc::now();
                        let token_expires = now + Duration::seconds(i64::from(response.expires_in));
                        let api = FeedlyApi::new(
                            secret_struct.client_id,
                            secret_struct.client_secret,
                            response.access_token.clone(),
                            response.refresh_token.clone(),
                            token_expires,
                        )
                        .context(FeedApiErrorKind::Api)?;
                        api.initialize_user_id(client).await.context(FeedApiErrorKind::Api)?;
                        let profile = api.get_profile(client).await.context(FeedApiErrorKind::Api)?;
                        if let Some(name) = &profile.given_name {
                            println!("{}", name);
                        }
                        self.config.write().set_access_token(&response.access_token);
                        self.config.write().set_refresh_token(&response.refresh_token);
                        self.config.write().set_token_expires(&token_expires.timestamp().to_string());
                        if let Some(user_name) = profile.given_name {
                            self.config.write().set_user_name(&user_name);
                        }
                        self.config.write().write().context(FeedApiErrorKind::Config)?;

                        self.api = Some(api);
                        self.logged_in = true;
                        return Ok(());
                    }
                    Err(e) => {
                        self.api = None;
                        self.logged_in = false;
                        return Err(e).context(FeedApiErrorKind::Login)?;
                    }
                },
                Err(e) => {
                    self.api = None;
                    self.logged_in = false;
                    return Err(e).context(FeedApiErrorKind::Login)?;
                }
            }
        }

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

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

    async fn initial_sync(&self, client: &Client) -> FeedApiResult<SyncResult> {
        if let Some(api) = &self.api {
            let tag_marked = api_call!(self, api, api.tag_marked(client).await, client);
            let tag_all = api_call!(self, api, api.category_all(client).await, client);

            let categories = api_call!(self, api, api.get_categories(client).await, client);
            let categories = Feedly::convert_category_vec(categories);

            let feeds = api_call!(self, api, api.get_subsriptions(client).await, client);
            let (feeds, mappings) = Feedly::convert_subscription_vec(feeds);
            let feed_ids: HashSet<FeedID> = feeds.iter().map(|f| f.feed_id.clone()).collect();

            let tags = api_call!(self, api, api.get_tags(client).await, client);
            let tags = Feedly::convert_tag_vec(tags);

            let mut articles: Vec<FatArticle> = Vec::new();
            let mut enclosures: Vec<Enclosure> = Vec::new();
            let mut taggings: Vec<Tagging> = Vec::new();

            // get starred articles
            let (mut starred_articles, mut starred_enclosures, mut starred_taggings) =
                self.get_articles(&tag_marked, Some(200), None, None, None, &feed_ids, client).await?;
            articles.append(&mut starred_articles);
            enclosures.append(&mut starred_enclosures);
            taggings.append(&mut starred_taggings);

            // get tagged articles
            for tag in &tags {
                let (mut tag_articles, mut tag_enclosures, mut tag_taggings) =
                    self.get_articles(tag.tag_id.to_str(), Some(200), None, None, None, &feed_ids, client).await?;
                articles.append(&mut tag_articles);
                enclosures.append(&mut tag_enclosures);
                taggings.append(&mut tag_taggings);
            }

            // get unread articles
            let (mut unread_articles, mut unread_enclosures, mut unread_taggings) =
                self.get_articles(&tag_all, Some(200), None, Some(true), None, &feed_ids, client).await?;
            articles.append(&mut unread_articles);
            enclosures.append(&mut unread_enclosures);
            taggings.append(&mut unread_taggings);

            return Ok(SyncResult {
                feeds: util::vec_to_option(feeds),
                categories: util::vec_to_option(categories),
                mappings: util::vec_to_option(mappings),
                tags: util::vec_to_option(tags),
                taggings: util::vec_to_option(taggings),
                headlines: None,
                articles: util::vec_to_option(articles),
                enclosures: util::vec_to_option(enclosures),
            });
        }
        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 tag_all = api_call!(self, api, api.category_all(client).await, client);
            let tag_marked = api_call!(self, api, api.tag_marked(client).await, client);

            let categories = api_call!(self, api, api.get_categories(client).await, client);
            let categories = Feedly::convert_category_vec(categories);

            let feeds = api_call!(self, api, api.get_subsriptions(client).await, client);
            let (feeds, mappings) = Feedly::convert_subscription_vec(feeds);
            let feed_ids: HashSet<FeedID> = feeds.iter().map(|f| f.feed_id.clone()).collect();

            let tags = api_call!(self, api, api.get_tags(client).await, client);
            let tags = Feedly::convert_tag_vec(tags);

            let mut articles: Vec<FatArticle> = Vec::new();
            let mut enclosures: Vec<Enclosure> = Vec::new();
            let mut taggings: Vec<Tagging> = Vec::new();

            // get recent articles
            let (mut recent_articles, mut recent_enclosures, mut recent_taggings) = self
                .get_articles(&tag_all, Some(200), None, None, Some(last_sync.timestamp() as u64), &feed_ids, client)
                .await?;
            articles.append(&mut recent_articles);
            enclosures.append(&mut recent_enclosures);
            taggings.append(&mut recent_taggings);

            // get starred articles
            let (mut starred_articles, mut starred_enclosures, mut starred_taggings) =
                self.get_articles(&tag_marked, Some(50), None, None, None, &feed_ids, client).await?;
            articles.append(&mut starred_articles);
            enclosures.append(&mut starred_enclosures);
            taggings.append(&mut starred_taggings);

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

    async fn set_article_read(&self, articles: &[ArticleID], read: models::Read, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let string_vec: Vec<&str> = articles.iter().map(|ref x| x.to_str()).collect();
            match read {
                models::Read::Read => api_call!(self, api, api.mark_entries_read(string_vec.clone(), client).await, client),
                models::Read::Unread => api_call!(self, api, api.mark_entries_unread(string_vec.clone(), client).await, client),
            };

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

    async fn set_article_marked(&self, articles: &[ArticleID], marked: models::Marked, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let string_vec: Vec<&str> = articles.iter().map(|ref x| x.to_str()).collect();
            match marked {
                models::Marked::Marked => api_call!(self, api, api.mark_entries_saved(string_vec.clone(), client).await, client),
                models::Marked::Unmarked => api_call!(self, api, api.mark_entries_unsaved(string_vec.clone(), client).await, client),
            };

            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 {
            let string_vec: Vec<&str> = feeds.iter().map(|ref x| x.to_str()).collect();
            api_call!(self, api, api.mark_feeds_read(string_vec.clone(), client).await, client);
            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 {
            let string_vec: Vec<&str> = categories.iter().map(|ref x| x.to_str()).collect();
            api_call!(self, api, api.mark_categories_read(string_vec.clone(), client).await, client);
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn set_tag_read(&self, tags: &[TagID], _last_sync: DateTime<Utc>, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let string_vec: Vec<&str> = tags.iter().map(|ref x| x.to_str()).collect();
            api_call!(self, api, api.mark_tags_read(string_vec.clone(), client).await, client);
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn set_all_read(&self, _last_sync: DateTime<Utc>, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let all = api_call!(self, api, api.category_all(client).await, client);
            let vec: Vec<&str> = vec![&all];
            api_call!(self, api, api.mark_categories_read(vec.clone(), client).await, client);
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn add_feed(
        &self,
        url: &Url,
        title: Option<String>,
        category: Option<CategoryID>,
        client: &Client,
    ) -> FeedApiResult<(Feed, Option<Category>)> {
        if let Some(api) = &self.api {
            let feed_id = FeedlyApi::gernerate_feed_id(url.get());

            let feed = SubscriptionInput {
                id: feed_id.clone(),
                title: match &title {
                    Some(title) => Some(title.clone()),
                    None => None,
                },
                categories: match category {
                    Some(category_id) => {
                        let category = FeedlyCategory {
                            id: category_id.to_string(),
                            label: None,
                            description: None,
                        };
                        Some(vec![category])
                    }
                    None => None,
                },
            };
            api_call!(self, api, api.add_subscription(feed.clone(), client).await, client);

            let feed_id = FeedID::new(&feed_id);
            let feed = match feed_parser::download_and_parse_feed(&url, &feed_id, title, None, client).await {
                Ok(ParsedUrl::SingleFeed(feed)) => feed,
                _ => {
                    // parsing went wrong -> remove feed from feedly account and return the error
                    self.remove_feed(&feed_id, client).await?;
                    return Err(FeedApiErrorKind::ParseFeed)?;
                }
            };
            return Ok((feed, None));
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn remove_feed(&self, id: &FeedID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            api_call!(self, api, api.delete_subscription(id.to_str(), client).await, client);
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn move_feed(&self, feed_id: &FeedID, from: &CategoryID, to: &CategoryID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let mappings = self.portal.get_mappings().context(FeedApiErrorKind::Portal)?;
            // all categories for feed_id except category 'from'
            let mut categories: Vec<FeedlyCategory> = mappings
                .into_iter()
                .filter(|mapping| &mapping.feed_id == feed_id && &mapping.category_id != from)
                .map(|mapping| FeedlyCategory {
                    id: mapping.category_id.to_string(),
                    label: None,
                    description: None,
                })
                .collect();

            // add category 'to'
            categories.push(FeedlyCategory {
                id: to.to_string(),
                label: None,
                description: None,
            });
            let feed = SubscriptionInput {
                id: feed_id.to_string(),
                title: None,
                categories: Some(categories),
            };
            api_call!(self, api, api.add_subscription(feed.clone(), client).await, client);
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn rename_feed(&self, feed_id: &FeedID, new_title: &str, client: &Client) -> FeedApiResult<FeedID> {
        if let Some(api) = &self.api {
            let feed = SubscriptionInput {
                id: feed_id.to_string(),
                title: Some(new_title.to_owned()),
                categories: None,
            };
            api_call!(self, api, api.add_subscription(feed.clone(), client).await, client);
            return Ok(feed_id.clone());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn add_category(&self, title: &str, parent: Option<&CategoryID>, client: &Client) -> FeedApiResult<CategoryID> {
        if let Some(api) = &self.api {
            if parent.is_some() {
                Err(FeedApiErrorKind::Unsupported)?
            }

            // only generate id
            // useing id as if it would exist will create category
            let category_id = api_call!(self, api, api.generate_category_id(&title, client).await, client);
            return Ok(CategoryID::new(&category_id));
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn remove_category(&self, id: &CategoryID, remove_children: bool, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            if remove_children {
                let mappings = self.portal.get_mappings().context(FeedApiErrorKind::Portal)?;

                let updated_subscriptions = mappings
                    .iter()
                    .filter(|m| &m.category_id == id)
                    .map(|m| SubscriptionInput {
                        id: m.feed_id.to_string(),
                        title: None,
                        categories: {
                            let categories = mappings
                                .iter()
                                .filter(|m2| m2.feed_id == m.feed_id && &m2.category_id != id)
                                .map(|m3| FeedlyCategory {
                                    id: m3.category_id.to_string(),
                                    label: None,
                                    description: None,
                                })
                                .collect::<Vec<FeedlyCategory>>();
                            Some(categories)
                        },
                    })
                    .collect::<Vec<SubscriptionInput>>();

                api_call!(self, api, api.update_subscriptions(updated_subscriptions.clone(), client).await, client);
            }
            api_call!(self, api, api.delete_category(id.to_str(), client).await, client);
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn rename_category(&self, id: &CategoryID, new_title: &str, client: &Client) -> FeedApiResult<CategoryID> {
        if let Some(api) = &self.api {
            let new_id = api_call!(self, api, api.generate_category_id(&new_title, client).await, client);
            api_call!(self, api, api.update_category(id.to_str(), new_title, client).await, client);
            return Ok(CategoryID::new(&new_id));
        }
        Err(FeedApiErrorKind::Login)?
    }

    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<()> {
        if let Some(api) = &self.api {
            api_call!(self, api, api.import_opml(opml, client).await, client);
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn add_tag(&self, title: &str, client: &Client) -> FeedApiResult<TagID> {
        if let Some(api) = &self.api {
            let id = api_call!(self, api, api.generate_tag_id(title, client).await, client);
            return Ok(TagID::new(&id));
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn remove_tag(&self, id: &TagID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            api_call!(self, api, api.delete_tags(vec![id.to_str()], client).await, client);
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn rename_tag(&self, id: &TagID, new_title: &str, client: &Client) -> FeedApiResult<TagID> {
        if let Some(api) = &self.api {
            let new_id = api_call!(self, api, api.generate_tag_id(&new_title, client).await, client);
            api_call!(self, api, api.update_tag(id.to_str(), new_title, client).await, client);
            return Ok(TagID::new(&new_id));
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn tag_article(&self, article_id: &ArticleID, tag_id: &TagID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            api_call!(self, api, api.tag_entry(article_id.to_str(), vec![tag_id.to_str()], client).await, client);
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn untag_article(&self, article_id: &ArticleID, tag_id: &TagID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            api_call!(
                self,
                api,
                api.untag_entries(vec![article_id.to_str()], vec![tag_id.to_str()], client).await,
                client
            );
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

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