mod config;
pub mod metadata;
mod oauth;

use std::collections::HashSet;
use std::sync::Arc;

use self::config::AccountConfig;
use self::metadata::InoreaderMetadata;
use self::oauth::InoreaderOAuth;
use crate::feed_api::{FeedApi, FeedApiError, FeedApiResult, FeedHeaderMap, Portal};
use crate::models::{
    self, ArticleID, Category, CategoryID, FavIcon, Feed, FeedID, FeedUpdateResult, Headline, LoginData, Marked, OAuthData, PluginCapabilities, Read,
    StreamConversionResult, SyncResult, TagID, Url,
};
use crate::util::greader::{ArticleQuery, GReaderUtil, TAG_READ_STR, TAG_READING_LIST, TAG_STARRED_STR};
use crate::{ParsedUrl, feed_parser, util};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use greader_api::models::{AuthInput, InoreaderAuthInput, ItemId, StreamType};
use greader_api::{ApiError, AuthData, GReaderApi};
use reqwest::Client;
use reqwest::header::{HeaderMap, HeaderValue};
use tokio::sync::RwLock;

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

impl Inoreader {
    async fn login_inoreader(&mut self, data: LoginData, client: &Client) -> FeedApiResult<()> {
        let LoginData::OAuth(data) = data else { return Err(FeedApiError::Login) };

        let oauth = InoreaderOAuth::new();

        let url = Url::parse(&data.url)?;
        let auth_code = oauth.parse_redirected_url(&url)?;

        self.config.write().await.set_custom_api_secret(data.custom_api_secret.as_ref());

        let (client_id, client_secret) = if let Some(user_api_secret) = data.custom_api_secret {
            // load previous custom API secrets from config
            (user_api_secret.client_id, user_api_secret.client_secret)
        } else {
            // no custom API secrets: use the default one
            (oauth.client_id, oauth.client_secret)
        };

        let api = GReaderApi::new(&oauth.base_uri, AuthData::Uninitialized);
        let auth_data = api
            .login(
                &AuthInput::Inoreader(InoreaderAuthInput {
                    auth_code,
                    redirect_url: oauth.redirect_uri,
                    client_id,
                    client_secret,
                }),
                client,
            )
            .await?;

        let auth_data = match auth_data {
            AuthData::Inoreader(auth_data) => auth_data,
            _ => return Err(FeedApiError::Login),
        };

        self.config.write().await.set_access_token(&auth_data.access_token);
        self.config.write().await.set_refresh_token(&auth_data.refresh_token);
        self.config.write().await.set_token_expires(&auth_data.expires_at.timestamp().to_string());

        let user = api.user_info(client).await?;
        self.config.write().await.set_user_name(&user.user_name);
        self.config.write().await.set_user_id(&user.user_id);
        self.config.write().await.write()?;
        self.api = Some(api);

        Ok(())
    }

    async fn is_token_expired(&self) -> Result<bool, ApiError> {
        let timestamp = self.config.write().await.get_token_expires().ok_or(ApiError::TokenExpired)?;
        let timestamp = timestamp.parse::<i64>().map_err(|_| ApiError::TokenExpired)?;

        let expires_at: DateTime<Utc> = util::timestamp_to_datetime(timestamp);
        let expires_in = expires_at.signed_duration_since(Utc::now());
        Ok(expires_in.num_seconds() <= 60)
    }

    async fn refresh_token(&self, api: &GReaderApi, client: &Client) -> FeedApiResult<()> {
        let response = api.inoreader_refresh_token(client).await?;
        let token_expires = response.expires_at;
        self.config.write().await.set_access_token(&response.access_token);
        self.config.write().await.set_token_expires(&token_expires.timestamp().to_string());
        self.config.write().await.write()?;
        Ok(())
    }

    async fn check_and_update_token(&self, api: &GReaderApi, client: &Client) -> FeedApiResult<()> {
        if self.is_token_expired().await? {
            self.refresh_token(api, client).await?;
        }

        Ok(())
    }
}

#[async_trait]
impl FeedApi for Inoreader {
    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_reachable(&self, client: &Client) -> FeedApiResult<bool> {
        let res = client.head("https://www.inoreader.com/").send().await?;
        Ok(res.status().is_success())
    }

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

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

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

        None
    }

    async fn login(&mut self, data: LoginData, client: &Client) -> FeedApiResult<()> {
        if let Err(error) = self.login_inoreader(data, client).await {
            tracing::error!(%error, "Failed to log in");
            self.api = None;
            self.logged_in = false;
            Err(error)
        } else {
            self.logged_in = true;
            Ok(())
        }
    }

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

    async fn initial_sync(&self, client: &Client, _custom_header: FeedHeaderMap) -> FeedApiResult<SyncResult> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            let stream_preferences = api.preference_stream_list(client);
            let feeds = api.subscription_list(client);
            let tags = api.tag_list(client);

            let (stream_preferences, feeds, tags) = futures::try_join!(stream_preferences, feeds, tags)?;

            let user_id = self.config.read().await.get_user_id();
            let (categories, category_mappings) =
                GReaderUtil::convert_category_vec(feeds.subscriptions.clone(), Some(&tags), Some(&stream_preferences), user_id.as_deref());
            let tags = GReaderUtil::convert_tag_list(tags, &categories);
            let tag_ids: HashSet<TagID> = tags.iter().map(|f| f.tag_id.clone()).collect();
            let (feeds, feed_mappings) = GReaderUtil::convert_feed_vec(feeds.subscriptions, Some(&stream_preferences));

            let unread = GReaderUtil::get_articles(
                api,
                client,
                self.portal.clone(),
                ArticleQuery {
                    stream_id: Some(TAG_READING_LIST),
                    read: Some(Read::Unread),
                    marked: None,
                    tag_ids: &tag_ids,
                    limit: None,
                    last_sync: None,
                },
            );

            let starred = GReaderUtil::get_articles(
                api,
                client,
                self.portal.clone(),
                ArticleQuery {
                    stream_id: Some(TAG_STARRED_STR),
                    read: None,
                    marked: None,
                    tag_ids: &tag_ids,
                    limit: None,
                    last_sync: None,
                },
            );

            let latest = GReaderUtil::get_articles(
                api,
                client,
                self.portal.clone(),
                ArticleQuery {
                    stream_id: None,
                    read: Some(Read::Read),
                    marked: Some(Marked::Unmarked),
                    tag_ids: &tag_ids,
                    limit: Some(100),
                    last_sync: None,
                },
            );

            let (unread, starred, latest) = futures::try_join!(unread, starred, latest)?;

            let mut result = StreamConversionResult::new();
            result.add(unread);
            result.add(starred);
            result.add(latest);

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

    async fn sync(&self, client: &Client, _custom_header: FeedHeaderMap) -> FeedApiResult<SyncResult> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp();

            let stream_preferences = api.preference_stream_list(client);
            let feeds = api.subscription_list(client);
            let tags = api.tag_list(client);

            let (stream_preferences, feeds, tags) = futures::try_join!(stream_preferences, feeds, tags)?;

            let user_id = self.config.read().await.get_user_id();
            let (categories, category_mappings) =
                GReaderUtil::convert_category_vec(feeds.subscriptions.clone(), Some(&tags), Some(&stream_preferences), user_id.as_deref());
            let tags = GReaderUtil::convert_tag_list(tags, &categories);
            let tag_ids: HashSet<TagID> = tags.iter().map(|f| f.tag_id.clone()).collect();
            let (feeds, feed_mappings) = GReaderUtil::convert_feed_vec(feeds.subscriptions, Some(&stream_preferences));

            let mut result = StreamConversionResult::new();

            // get all new articles since last sync
            let new_unread = GReaderUtil::get_articles(
                api,
                client,
                self.portal.clone(),
                ArticleQuery {
                    stream_id: Some(TAG_READING_LIST),
                    read: Some(Read::Unread),
                    marked: None,
                    tag_ids: &tag_ids,
                    limit: None,
                    last_sync: Some(last_sync),
                },
            );

            // get all new starred since last sync
            let new_marked = GReaderUtil::get_articles(
                api,
                client,
                self.portal.clone(),
                ArticleQuery {
                    stream_id: Some(TAG_STARRED_STR),
                    read: None,
                    marked: None,
                    tag_ids: &tag_ids,
                    limit: None,
                    last_sync: Some(last_sync),
                },
            );

            // unread article ids
            let inoreader_unread_ids = GReaderUtil::get_article_ids(api, client, Some(TAG_READING_LIST), Some(Read::Unread), None, None);

            // marked (saved/starred) article ids
            let inoreader_marked_ids = GReaderUtil::get_article_ids(api, client, Some(TAG_STARRED_STR), None, None, None);

            let (new_unread, new_marked, inoreader_unread_ids, inoreader_marked_ids) =
                futures::try_join!(new_unread, new_marked, inoreader_unread_ids, inoreader_marked_ids)?;

            result.add(new_unread);
            result.add(new_marked);

            let inoreader_unread_ids: HashSet<ArticleID> = inoreader_unread_ids
                .into_iter()
                .map(|item_id| {
                    let ItemId { id } = item_id;
                    ArticleID::from_owned(id)
                })
                .collect();

            let inoreader_marked_ids: HashSet<ArticleID> = inoreader_marked_ids
                .into_iter()
                .map(|item_id| {
                    let ItemId { id } = item_id;
                    ArticleID::from_owned(id)
                })
                .collect();

            // get local unread
            let local_unread_ids = self.portal.get_article_ids_unread_all()?;
            let local_unread_ids = local_unread_ids.into_iter().collect::<HashSet<_>>();

            // mark remotely read article as read
            let mut should_mark_read_headlines = local_unread_ids
                .difference(&inoreader_unread_ids)
                .cloned()
                .map(|id| {
                    let marked = if inoreader_marked_ids.contains(&id) {
                        Marked::Marked
                    } else {
                        Marked::Unmarked
                    };

                    Headline {
                        article_id: id,
                        unread: Read::Read,
                        marked,
                    }
                })
                .collect();
            result.headlines.append(&mut should_mark_read_headlines);

            // get local marked
            let local_marked_ids = self.portal.get_article_ids_marked_all()?;
            let local_marked_ids = local_marked_ids.into_iter().collect::<HashSet<_>>();

            // mark remotly starred articles locally
            let mut mark_headlines = local_marked_ids
                .difference(&inoreader_marked_ids)
                .map(|id| Headline {
                    article_id: id.clone(),
                    marked: Marked::Marked,
                    unread: if inoreader_unread_ids.contains(id) { Read::Unread } else { Read::Read },
                })
                .collect();
            result.headlines.append(&mut mark_headlines);

            // unmark remotly unstarred articles locally
            let mut missing_unmarked_headlines = local_marked_ids
                .difference(&inoreader_marked_ids)
                .cloned()
                .map(|id| {
                    let unread = if inoreader_unread_ids.contains(&id) { Read::Unread } else { Read::Read };

                    Headline {
                        article_id: id,
                        marked: Marked::Unmarked,
                        unread,
                    }
                })
                .collect();
            result.headlines.append(&mut missing_unmarked_headlines);

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

    async fn fetch_feed(&self, feed_id: &FeedID, client: &Client, _custom_header: HeaderMap<HeaderValue>) -> FeedApiResult<FeedUpdateResult> {
        if let Some(api) = &self.api {
            let tags = api.tag_list(client);
            let feeds = api.subscription_list(client);

            let (feeds, tags) = futures::try_join!(feeds, tags)?;

            let (categories, _category_mappings) = GReaderUtil::convert_category_vec(feeds.subscriptions.clone(), Some(&tags), None, None);
            let tags = GReaderUtil::convert_tag_list(tags, &categories);
            let tag_ids: HashSet<TagID> = tags.iter().map(|f| f.tag_id.clone()).collect();
            let (feeds, _feed_mappings) = GReaderUtil::convert_feed_vec(feeds.subscriptions, None);
            let feed = feeds.iter().find(|feed| &feed.feed_id == feed_id).cloned();

            let result = GReaderUtil::get_articles(
                api,
                client,
                self.portal.clone(),
                ArticleQuery {
                    stream_id: Some(feed_id.as_str()),
                    read: None,
                    marked: None,
                    tag_ids: &tag_ids,
                    limit: Some(20),
                    last_sync: None,
                },
            )
            .await?;

            return Ok(FeedUpdateResult {
                feed,
                taggings: crate::util::vec_to_option(result.taggings),
                articles: crate::util::vec_to_option(result.articles),
                enclosures: crate::util::vec_to_option(result.enclosures),
            });
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn set_article_read(&self, articles: &[ArticleID], read: models::Read, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            let item_ids: Vec<_> = articles.iter().map(|id| id.as_str()).collect();
            let (add_tag, remove_tag) = match read {
                Read::Read => (Some(TAG_READ_STR), None),
                Read::Unread => (None, Some(TAG_READ_STR)),
            };
            api.tag_edit(&item_ids, add_tag, remove_tag, client).await?;
            Ok(())
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn set_article_marked(&self, articles: &[ArticleID], marked: models::Marked, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            let item_ids: Vec<_> = articles.iter().map(|id| id.as_str()).collect();
            let (add_tag, remove_tag) = match marked {
                Marked::Marked => (Some(TAG_STARRED_STR), None),
                Marked::Unmarked => (None, Some(TAG_STARRED_STR)),
            };
            api.tag_edit(&item_ids, add_tag, remove_tag, client).await?;
            Ok(())
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn set_feed_read(&self, feeds: &[FeedID], _articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp_micros() as u64;
            let mut mark_read_futures = Vec::new();

            for feed in feeds {
                mark_read_futures.push(api.mark_all_as_read(feed.as_str(), Some(last_sync), client));
            }

            futures::future::try_join_all(mark_read_futures).await?;
            Ok(())
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn set_category_read(&self, categories: &[CategoryID], _articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp_micros() as u64;
            let mut mark_read_futures = Vec::new();

            for category in categories {
                mark_read_futures.push(api.mark_all_as_read(category.as_str(), Some(last_sync), client));
            }

            futures::future::try_join_all(mark_read_futures).await?;
            Ok(())
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn set_tag_read(&self, tags: &[TagID], _articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp_micros() as u64;
            let mut mark_read_futures = Vec::new();

            for tag in tags {
                mark_read_futures.push(api.mark_all_as_read(tag.as_str(), Some(last_sync), client));
            }

            futures::future::try_join_all(mark_read_futures).await?;
            Ok(())
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn set_all_read(&self, articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
        self.set_article_read(articles, Read::Read, client).await
    }

    async fn add_feed(
        &self,
        url: &Url,
        title: Option<String>,
        category_id: Option<CategoryID>,
        client: &Client,
    ) -> FeedApiResult<(Feed, Option<Category>)> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            let feed_id = format!("feed/{}", url.as_str());
            let category_str = category_id.clone().map(|id| id.as_str().to_owned());

            api.subscription_create(url, title.as_deref(), category_str.as_deref(), client).await?;

            let feed_id = FeedID::new(&feed_id);
            let semaphore = self.portal.get_download_semaphore();
            let result = feed_parser::download_and_parse_feed(url, &feed_id, title, semaphore, client).await;

            if result.is_err() {
                tracing::warn!("parsing went wrong -> remove feed from freshrss account");
                self.remove_feed(&feed_id, client).await?;
            }

            let feed = match result? {
                ParsedUrl::SingleFeed(feed) => feed,
                _ => {
                    let msg = "Expected Single Feed";
                    tracing::warn!("{msg}");
                    return Err(FeedApiError::Api { message: msg.into() });
                }
            };

            // return category in case a new one got created
            let local_categories = self.portal.get_categories()?;
            let category = if !local_categories.iter().any(|c| Some(&c.category_id) == category_id.as_ref()) {
                let feeds = api.subscription_list(client).await?;
                let user_id = self.config.read().await.get_user_id();
                let (categories, _category_mappings) = GReaderUtil::convert_category_vec(feeds.subscriptions, None, None, user_id.as_deref());
                categories.iter().find(|c| Some(&c.category_id) == category_id.as_ref()).cloned()
            } else {
                None
            };

            return Ok((*feed, category));
        }
        Err(FeedApiError::Login)
    }

    async fn remove_feed(&self, id: &FeedID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            let feed_id_str = id.as_str();
            api.subscription_delete(feed_id_str, client).await?;
            Ok(())
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn move_feed(&self, feed_id: &FeedID, from: &CategoryID, to: &CategoryID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            api.subscription_edit(feed_id.as_str(), None, Some(from.as_str()), Some(to.as_str()), client)
                .await?;
            Ok(())
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn rename_feed(&self, feed_id: &FeedID, new_title: &str, client: &Client) -> FeedApiResult<FeedID> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            api.subscription_edit(feed_id.as_str(), Some(new_title), None, None, client).await?;
            Ok(feed_id.clone())
        } else {
            Err(FeedApiError::Login)
        }
    }

    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> {
        if let Some(_api) = &self.api {
            if parent.is_some() {
                return Err(FeedApiError::Unsupported);
            }

            // only generate id
            // useing id as if it would exist will create category
            let user_id = self.config.read().await.get_user_id();
            let category_id = GReaderUtil::generate_tag_id(user_id.as_deref(), title);
            Ok(CategoryID::new(&category_id))
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn remove_category(&self, id: &CategoryID, remove_children: bool, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            if remove_children {
                let mappings = self.portal.get_feed_mappings()?;

                let feed_ids = mappings
                    .iter()
                    .filter(|m| &m.category_id == id)
                    .map(|m| m.feed_id.clone())
                    .collect::<Vec<FeedID>>();

                for feed_id in feed_ids {
                    self.remove_feed(&feed_id, client).await?;
                }
            }

            api.tag_delete(StreamType::Category, id.as_str(), client).await?;
            Ok(())
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn rename_category(&self, id: &CategoryID, new_title: &str, client: &Client) -> FeedApiResult<CategoryID> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            api.tag_rename(StreamType::Category, id.as_str(), new_title, client).await?;

            let user_id = self.config.read().await.get_user_id();
            Ok(CategoryID::new(&GReaderUtil::generate_tag_id(user_id.as_deref(), new_title)))
        } else {
            Err(FeedApiError::Login)
        }
    }

    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<()> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;

            api.import(opml.to_owned(), client).await?;
            Ok(())
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn add_tag(&self, title: &str, _client: &Client) -> FeedApiResult<TagID> {
        if let Some(_api) = &self.api {
            // only generate id
            // useing id as if it would exist will create tag
            let user_id = self.config.read().await.get_user_id();
            let tag_id = GReaderUtil::generate_tag_id(user_id.as_deref(), title);
            Ok(TagID::new(&tag_id))
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn remove_tag(&self, id: &TagID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;
            api.tag_delete(StreamType::Stream, id.as_str(), client).await?;
            Ok(())
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn rename_tag(&self, id: &TagID, new_title: &str, client: &Client) -> FeedApiResult<TagID> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;
            api.tag_rename(StreamType::Stream, id.as_str(), new_title, client).await?;

            let user_id = self.config.read().await.get_user_id();
            Ok(TagID::new(&GReaderUtil::generate_tag_id(user_id.as_deref(), new_title)))
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn tag_article(&self, article_id: &ArticleID, tag_id: &TagID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;
            api.tag_edit(&[article_id.as_str()], Some(tag_id.as_str()), None, client).await?;
            Ok(())
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn untag_article(&self, article_id: &ArticleID, tag_id: &TagID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            self.check_and_update_token(api, client).await?;
            api.tag_edit(&[article_id.as_str()], None, Some(tag_id.as_str()), client).await?;
            Ok(())
        } else {
            Err(FeedApiError::Login)
        }
    }

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