pub mod config;
pub mod metadata;

use self::config::AccountConfig;
use self::metadata::FeedbinMetadata;
use crate::feed_api::{FeedApi, FeedApiError, FeedApiErrorKind, FeedApiResult, Portal};
use crate::models;
use crate::models::{
    ArticleID, Category, CategoryID, CategoryType, Enclosure, EnclosureType, FatArticle, FavIcon, Feed, FeedID, FeedMapping, Headline, LoginData,
    Marked, PasswordLogin, PluginCapabilities, Read, SyncResult, TagID, Url, NEWSFLASH_TOPLEVEL,
};
use crate::util;
use crate::util::html2text::Html2Text;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use failure::ResultExt;
use feedbin_api::models::{
    CacheRequestResponse, CacheResult, CreateSubscriptionResult, Entry, Icon as FeedbinIcon, Subscription, Tagging as FeedbinTagging,
};
use feedbin_api::{EntryID, FeedbinApi};
use log::info;
use parking_lot::RwLock;
use reqwest::Client;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use url::Host;

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

impl Feedbin {
    fn api_subdomain_url(url: &Url) -> Option<Url> {
        if let Some(host) = url.get().host().to_owned() {
            if let Host::Domain(host_string) = host {
                if !host_string.starts_with("api.") {
                    let mut api_url = url.get();
                    api_url.set_host(Some(&format!("api.{}", host_string))).ok();
                    let api_url = Url::new(api_url);
                    return Some(api_url);
                }
            }
        }

        None
    }

    fn taggings_to_categories(&self, taggings: &CacheRequestResponse<Vec<FeedbinTagging>>) -> FeedApiResult<Vec<Category>> {
        let taggings = match taggings {
            CacheRequestResponse::NotModified => return self.portal.get_categories().map_err(|_| FeedApiError::from(FeedApiErrorKind::Portal)),
            CacheRequestResponse::Modified(CacheResult {
                value: taggings,
                cache: _cache,
            }) => taggings,
        };
        let category_names: HashSet<String> = taggings.iter().map(|t| t.name.clone()).collect();
        Ok(category_names
            .into_iter()
            .enumerate()
            .map(|(i, n)| Category {
                category_id: CategoryID::new(&n),
                label: n,
                parent_id: NEWSFLASH_TOPLEVEL.clone(),
                sort_index: Some(i as i32),
                category_type: CategoryType::Default,
            })
            .collect())
    }

    fn taggings_to_feedmappings(&self, taggings: &CacheRequestResponse<Vec<FeedbinTagging>>) -> FeedApiResult<Vec<FeedMapping>> {
        let taggings = match taggings {
            CacheRequestResponse::NotModified => return self.portal.get_mappings().map_err(|_| FeedApiError::from(FeedApiErrorKind::Portal)),
            CacheRequestResponse::Modified(CacheResult {
                value: taggings,
                cache: _cache,
            }) => taggings,
        };
        Ok(taggings
            .iter()
            .map(|t| FeedMapping {
                feed_id: FeedID::new(&t.feed_id.to_string()),
                category_id: CategoryID::new(&t.name.clone()),
            })
            .collect())
    }

    fn subscriptions_to_feeds(&self, subscriptions: Vec<Subscription>, icons: Vec<FeedbinIcon>) -> Vec<Feed> {
        let icon_map: HashMap<String, String> = icons.into_iter().map(|i| (i.host, i.url)).collect();
        subscriptions
            .into_iter()
            .map(move |s| {
                let website = Url::parse(&s.site_url).ok();
                let feed_url = Url::parse(&s.feed_url).ok();
                let icon_url = website
                    .clone()
                    .map(|url| url.host().map(|s| s.to_owned()))
                    .flatten()
                    .map(|host| icon_map.get(&host))
                    .flatten()
                    .map(|icon_url| Url::parse(icon_url).ok())
                    .flatten();
                Feed {
                    feed_id: FeedID::new(&s.feed_id.to_string()),
                    label: s.title,
                    website,
                    feed_url,
                    icon_url,
                    sort_index: None,
                }
            })
            .collect()
    }

    fn entries_to_articles(
        entries: Vec<Entry>,
        unread_entry_ids: &HashSet<EntryID>,
        starred_entry_ids: &HashSet<EntryID>,
    ) -> (Vec<FatArticle>, Vec<Enclosure>) {
        let mut enclosures: Vec<Enclosure> = Vec::new();
        let articles = entries
            .into_iter()
            .map(|e| {
                if let Some(enclosure) = e.enclosure {
                    if let Ok(url) = Url::parse(&enclosure.enclosure_url) {
                        enclosures.push(Enclosure {
                            article_id: ArticleID::new(&e.id.to_string()),
                            url,
                            enclosure_type: if enclosure.enclosure_type.starts_with("audio") {
                                EnclosureType::Audio
                            } else if enclosure.enclosure_type.starts_with("video") {
                                EnclosureType::Video
                            } else if enclosure.enclosure_type.starts_with("image") {
                                EnclosureType::Image
                            } else {
                                EnclosureType::File
                            },
                        });
                    }
                }
                let plain_text = match &e.content {
                    Some(content) => Html2Text::process(content),
                    None => Some(e.summary.clone()),
                };
                FatArticle {
                    article_id: ArticleID::new(&e.id.to_string()),
                    title: e.title,
                    author: e.author,
                    feed_id: FeedID::new(&e.feed_id.to_string()),
                    url: Url::parse(&e.url).ok(),
                    date: match DateTime::parse_from_str(&e.published, "%+") {
                        Ok(date) => date.with_timezone(&Utc),
                        Err(_) => Utc::now(),
                    }
                    .naive_utc(),
                    html: match e.content {
                        Some(content) => Some(content),
                        None => Some(e.summary.clone()),
                    },
                    summary: Some(e.summary),
                    direction: None,
                    unread: if unread_entry_ids.contains(&e.id) { Read::Unread } else { Read::Read },
                    marked: if starred_entry_ids.contains(&e.id) {
                        Marked::Marked
                    } else {
                        Marked::Unmarked
                    },
                    scraped_content: None,
                    plain_text,
                }
            })
            .collect();

        (articles, enclosures)
    }

    fn article_id_to_entry_id(article_ids: &[ArticleID]) -> Vec<EntryID> {
        article_ids.iter().filter_map(|id| id.to_str().parse::<EntryID>().ok()).collect()
    }
}

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

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

    async fn is_logged_in(&self, client: &Client) -> FeedApiResult<bool> {
        match &self.api {
            None => Ok(false),
            Some(api) => {
                let authenticated = api.is_authenticated(client).await.context(FeedApiErrorKind::Api)?;
                Ok(authenticated)
            }
        }
    }

    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() {
            if let Some(username) = self.config.read().get_user_name() {
                if let Some(password) = self.config.read().get_password() {
                    return Some(LoginData::Password(PasswordLogin {
                        id: FeedbinMetadata::get_id(),
                        url: self.config.read().get_url(),
                        user: username,
                        password: password,
                        http_user: None, // feedbin authentication already uses basic auth
                        http_password: None,
                    }));
                }
            }
        }

        None
    }

    async fn login(&mut self, data: LoginData, client: &Client) -> FeedApiResult<()> {
        self.logged_in = false;
        self.api = None;

        if let LoginData::Password(data) = data {
            let mut url_string = data.url.clone().ok_or(FeedApiErrorKind::Url)?;
            let url = Url::parse(&url_string).context(FeedApiErrorKind::Url)?;
            let mut api = FeedbinApi::new(url.get(), data.user.clone(), data.password.clone());

            let mut auth_req = api.is_authenticated(client).await;
            if auth_req.is_err() {
                if let Some(api_url) = Self::api_subdomain_url(&url) {
                    info!("Trying to authenticate with base url: {}", api_url);
                    api = FeedbinApi::new(api_url.get(), data.user.clone(), data.password.clone());
                    auth_req = api.is_authenticated(client).await;
                    if auth_req.is_err() {
                        return Err(FeedApiErrorKind::Login)?;
                    } else {
                        url_string = api_url.to_string();
                    }
                } else {
                    return Err(FeedApiErrorKind::Login)?;
                }
            }

            if let Ok(true) = auth_req {
                self.config.write().set_url(&url_string);
                self.config.write().set_password(&data.password);
                self.config.write().set_user_name(&data.user);
                self.config.read().save()?;
                self.api = Some(api);
                self.logged_in = true;
                return Ok(());
            }
        }

        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 subscription_cache = self.config.read().get_subscription_cache();
            let taggings_cache = self.config.read().get_taggins_cache();

            let subscriptions = api
                .get_subscriptions(client, None, None, subscription_cache)
                .await
                .context(FeedApiErrorKind::Api)?;
            let taggings = api.get_taggings(client, taggings_cache).await.context(FeedApiErrorKind::Api)?;

            self.config.write().set_subscription_cache(&subscriptions);
            self.config.write().set_taggins_cache(&taggings);
            self.config.read().save().context(FeedApiErrorKind::Config)?;

            let feeds = match subscriptions {
                CacheRequestResponse::NotModified => self.portal.get_feeds().context(FeedApiErrorKind::Api)?,
                CacheRequestResponse::Modified(CacheResult {
                    value: subscriptions,
                    cache: _cache,
                }) => self.subscriptions_to_feeds(subscriptions, api.get_icons(client).await.context(FeedApiErrorKind::Api)?),
            };

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

            let unread_entry_ids = api.get_unread_entry_ids(client).await.context(FeedApiErrorKind::Api)?;
            let starred_entry_ids = api.get_starred_entry_ids(client).await.context(FeedApiErrorKind::Api)?;

            let unread_entry_id_set: HashSet<EntryID> = unread_entry_ids.iter().copied().collect();
            let starred_entry_id_set: HashSet<EntryID> = starred_entry_ids.iter().copied().collect();

            let entry_ids_total: Vec<EntryID> = unread_entry_id_set.union(&starred_entry_id_set).copied().collect();

            for entry_ids_total_chunk in entry_ids_total.chunks(100) {
                let entries_total_chunk = api
                    .get_entries(client, None, None, Some(entry_ids_total_chunk), None, Some(true))
                    .await
                    .context(FeedApiErrorKind::Api)?;
                let (mut total_articles, mut total_enclosures) =
                    Self::entries_to_articles(entries_total_chunk, &unread_entry_id_set, &starred_entry_id_set);
                articles.append(&mut total_articles);
                enclosures.append(&mut total_enclosures);
            }

            return Ok(SyncResult {
                feeds: util::vec_to_option(feeds),
                categories: util::vec_to_option(self.taggings_to_categories(&taggings)?),
                mappings: util::vec_to_option(self.taggings_to_feedmappings(&taggings)?),
                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 subscription_cache = self.config.read().get_subscription_cache();
            let taggings_cache = self.config.read().get_taggins_cache();

            let subscriptions = api
                .get_subscriptions(client, None, None, subscription_cache)
                .await
                .context(FeedApiErrorKind::Api)?;
            let taggings = api.get_taggings(client, taggings_cache).await.context(FeedApiErrorKind::Api)?;

            self.config.write().set_subscription_cache(&subscriptions);
            self.config.write().set_taggins_cache(&taggings);
            self.config.read().save().context(FeedApiErrorKind::Config)?;

            let feeds = match subscriptions {
                CacheRequestResponse::NotModified => self.portal.get_feeds().context(FeedApiErrorKind::Api)?,
                CacheRequestResponse::Modified(CacheResult {
                    value: subscriptions,
                    cache: _cache,
                }) => self.subscriptions_to_feeds(subscriptions, api.get_icons(client).await.context(FeedApiErrorKind::Api)?),
            };

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

            let unread_entry_ids = api.get_unread_entry_ids(client).await.context(FeedApiErrorKind::Api)?;
            let starred_entry_ids = api.get_starred_entry_ids(client).await.context(FeedApiErrorKind::Api)?;

            let unread_entry_id_set: HashSet<EntryID> = unread_entry_ids.iter().copied().collect();
            let starred_entry_id_set: HashSet<EntryID> = starred_entry_ids.iter().copied().collect();

            let local_unread_ids = self.portal.get_article_ids_unread_all().context(FeedApiErrorKind::Portal)?;
            let local_unread_ids = Self::article_id_to_entry_id(&local_unread_ids);
            let local_unread_ids = local_unread_ids.into_iter().collect();

            let local_marked_ids = self.portal.get_article_ids_marked_all().context(FeedApiErrorKind::Portal)?;
            let local_marked_ids = Self::article_id_to_entry_id(&local_marked_ids);
            let local_marked_ids = local_marked_ids.into_iter().collect();

            let missing_unread_ids: HashSet<EntryID> = unread_entry_id_set.difference(&local_unread_ids).cloned().collect();
            let missing_marked_ids: HashSet<EntryID> = starred_entry_id_set.difference(&local_marked_ids).cloned().collect();

            // sync new unread/marked articles
            let missing_ids: Vec<EntryID> = missing_marked_ids.union(&missing_unread_ids).copied().collect();
            for missing_ids_chunk in missing_ids.chunks(100) {
                let missing_entry_chunk = api
                    .get_entries(client, None, None, Some(missing_ids_chunk), None, Some(true))
                    .await
                    .context(FeedApiErrorKind::Api)?;
                let (mut new_articles, mut new_enclosures) =
                    Self::entries_to_articles(missing_entry_chunk, &unread_entry_id_set, &starred_entry_id_set);
                articles.append(&mut new_articles);
                enclosures.append(&mut new_enclosures);
            }

            // mark remotely read article as read
            let should_mark_read_ids: Vec<u64> = local_unread_ids.difference(&unread_entry_id_set).copied().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 starred_entry_id_set.contains(&id) {
                        Marked::Marked
                    } else {
                        Marked::Unmarked
                    },
                })
                .collect();
            headlines.append(&mut should_mark_read_headlines);

            // unmark remotly unstarred articles locally
            let missing_unmarked_ids: Vec<u64> = local_marked_ids.difference(&starred_entry_id_set).copied().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 unread_entry_id_set.contains(&id) { Read::Unread } else { Read::Read },
                })
                .collect();
            headlines.append(&mut missing_unmarked_headlines);

            // latest articles
            let entries = api
                .get_entries(client, None, Some(last_sync), None, None, Some(true))
                .await
                .context(FeedApiErrorKind::Api)?;
            let (mut latest_articles, mut latest_enclosures) = Self::entries_to_articles(entries, &unread_entry_id_set, &starred_entry_id_set);
            articles.append(&mut latest_articles);
            enclosures.append(&mut latest_enclosures);

            return Ok(SyncResult {
                feeds: util::vec_to_option(feeds),
                categories: util::vec_to_option(self.taggings_to_categories(&taggings)?),
                mappings: util::vec_to_option(self.taggings_to_feedmappings(&taggings)?),
                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: models::Read, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            match read {
                Read::Unread => api
                    .set_entries_unread(client, &Self::article_id_to_entry_id(articles))
                    .await
                    .context(FeedApiErrorKind::Api)?,
                Read::Read => api
                    .set_entries_read(client, &Self::article_id_to_entry_id(articles))
                    .await
                    .context(FeedApiErrorKind::Api)?,
            }

            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 {
            match marked {
                Marked::Unmarked => api
                    .set_entries_unstarred(client, &Self::article_id_to_entry_id(articles))
                    .await
                    .context(FeedApiErrorKind::Api)?,
                Marked::Marked => api
                    .set_entries_starred(client, &Self::article_id_to_entry_id(articles))
                    .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 {
            let mut entry_ids: Vec<EntryID> = Vec::new();
            for feed_id in feeds {
                let article_ids = self.portal.get_article_ids_unread_feed(feed_id).context(FeedApiErrorKind::Portal)?;
                entry_ids.append(&mut Self::article_id_to_entry_id(&article_ids));
            }

            for entry_id_chunk in entry_ids.chunks(1000) {
                api.set_entries_read(client, entry_id_chunk).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 {
            let mut entry_ids: Vec<EntryID> = Vec::new();
            for category_id in categories {
                let article_ids = self
                    .portal
                    .get_article_ids_unread_category(category_id)
                    .context(FeedApiErrorKind::Portal)?;
                entry_ids.append(&mut Self::article_id_to_entry_id(&article_ids));
            }

            for entry_id_chunk in entry_ids.chunks(1000) {
                api.set_entries_read(client, entry_id_chunk).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 {
            let article_ids = self.portal.get_article_ids_unread_all().context(FeedApiErrorKind::Portal)?;
            let entry_ids = Self::article_id_to_entry_id(&article_ids);

            for entry_id_chunk in entry_ids.chunks(1000) {
                api.set_entries_read(client, entry_id_chunk).await.context(FeedApiErrorKind::Api)?;
            }

            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 res = api.create_subscription(client, url.to_string()).await.context(FeedApiErrorKind::Api)?;
            match res {
                CreateSubscriptionResult::NotFound | CreateSubscriptionResult::MultipleOptions(_) => return Err(FeedApiErrorKind::Unsupported)?,
                CreateSubscriptionResult::Found(_url) => return Err(FeedApiErrorKind::Api)?,
                CreateSubscriptionResult::Created(mut subscription) => {
                    let icons = api.get_icons(client).await.context(FeedApiErrorKind::Api)?;
                    if let Some(title) = title {
                        if subscription.title != title {
                            api.update_subscription(client, subscription.id, &title)
                                .await
                                .context(FeedApiErrorKind::Api)?;
                            subscription.title = title;
                        }
                    }
                    let mut res_category: Option<Category> = None;
                    if let Some(category_id) = category {
                        let title = category_id.to_string();
                        api.create_tagging(client, subscription.feed_id, &title)
                            .await
                            .context(FeedApiErrorKind::Api)?;
                        res_category = Some(Category {
                            category_id,
                            label: title,
                            parent_id: NEWSFLASH_TOPLEVEL.clone(),
                            sort_index: None,
                            category_type: CategoryType::Default,
                        });
                    }
                    let feed = self.subscriptions_to_feeds(vec![subscription], icons);

                    return Ok((feed.first().expect("expected to be 1 feed exactly").clone(), res_category));
                }
            }
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn remove_feed(&self, id: &FeedID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let feed_id = id.to_str().parse::<u64>().context(FeedApiErrorKind::Unknown)?;
            let subscriptions = match api.get_subscriptions(client, None, None, None).await.context(FeedApiErrorKind::Api)? {
                CacheRequestResponse::Modified(CacheResult {
                    value: subscriptions,
                    cache: _cache,
                }) => subscriptions,
                CacheRequestResponse::NotModified => return Err(FeedApiErrorKind::Api)?,
            };
            let subscription_id = subscriptions.iter().find(|s| s.feed_id == feed_id).map(|s| s.id);
            if let Some(subscription_id) = subscription_id {
                api.delete_subscription(client, subscription_id).await.context(FeedApiErrorKind::Api)?;
            }
            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 feed_id = feed_id.to_str().parse::<u64>().context(FeedApiErrorKind::Unknown)?;
            if from != &NEWSFLASH_TOPLEVEL.clone() {
                let taggings = match api.get_taggings(client, None).await.context(FeedApiErrorKind::Api)? {
                    CacheRequestResponse::Modified(CacheResult {
                        value: taggings,
                        cache: _cache,
                    }) => taggings,
                    CacheRequestResponse::NotModified => return Err(FeedApiErrorKind::Api)?,
                };
                let tagging_id = taggings.iter().find(|t| t.name == from.to_str() && t.feed_id == feed_id).map(|t| t.id);
                if let Some(tagging_id) = tagging_id {
                    api.delete_tagging(client, tagging_id).await.context(FeedApiErrorKind::Api)?;
                }
            }

            api.create_tagging(client, feed_id, to.to_str()).await.context(FeedApiErrorKind::Api)?;

            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 subscriptions = match api.get_subscriptions(client, None, None, None).await.context(FeedApiErrorKind::Api)? {
                CacheRequestResponse::Modified(CacheResult {
                    value: subscriptions,
                    cache: _cache,
                }) => subscriptions,
                CacheRequestResponse::NotModified => return Err(FeedApiErrorKind::Api)?,
            };
            let subscription_id = subscriptions
                .iter()
                .find(|s| s.feed_id.to_string() == feed_id.to_str())
                .map(|s| s.id)
                .expect("Failed to get subscription ID");

            api.update_subscription(client, subscription_id, new_title)
                .await
                .context(FeedApiErrorKind::Api)?;
            return Ok(feed_id.clone());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn add_category(&self, title: &str, _parent: Option<&CategoryID>, _client: &Client) -> FeedApiResult<CategoryID> {
        Ok(CategoryID::new(title))
    }

    async fn remove_category(&self, id: &CategoryID, remove_children: bool, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            api.delete_tag(client, id.to_str()).await.context(FeedApiErrorKind::Api)?;
            if remove_children {
                let mappings = self.portal.get_mappings().context(FeedApiErrorKind::Portal)?;
                for mapping in mappings {
                    if &mapping.category_id == id {
                        self.remove_feed(&mapping.feed_id, client).await?;
                    }
                }
            }
            return Ok(());
        }
        Err(FeedApiErrorKind::Unsupported)?
    }

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

    async fn rename_category(&self, id: &CategoryID, new_title: &str, client: &Client) -> FeedApiResult<CategoryID> {
        if let Some(api) = &self.api {
            api.rename_tag(client, id.to_str(), new_title).await.context(FeedApiErrorKind::Api)?;
            return Ok(id.clone());
        }
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn import_opml(&self, opml: &str, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            api.import_opml(client, opml).await.context(FeedApiErrorKind::Api)?;
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    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> {
        Err(FeedApiErrorKind::Unsupported)?
    }
}
