mod error;
pub mod models;
#[cfg(test)]
mod tests;

pub use self::error::ApiError;
use self::models::{
    AccessTokenResponse, Category, Collection, CollectionFeedInput, CollectionInput, Counts, Entry,
    FeedlyError, Profile, ProfileUpdate, RefreshTokenResponse, SearchResult, Stream, Subscription,
    SubscriptionInput, Tag,
};
use chrono::{DateTime, TimeDelta, Utc};
use log::info;
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use tokio::sync::Mutex;
use url::Url;

pub type AuthCode = String;
pub type AccessToken = String;
pub type RefreshToken = String;

const FEEDLY_ENCODE_SET: &AsciiSet = &CONTROLS
    .add(b' ')
    .add(b'"')
    .add(b'<')
    .add(b'>')
    .add(b'`')
    .add(b'#')
    .add(b'?')
    .add(b'{')
    .add(b'}')
    .add(b'/')
    .add(b':')
    .add(b';')
    .add(b'=')
    .add(b'@')
    .add(b'[')
    .add(b']')
    .add(b'\\')
    .add(b'^')
    .add(b'|')
    .add(b'+');

pub struct FeedlyApi {
    base_uri: Url,
    client_id: String,
    client_secret: String,
    user_id: Arc<Mutex<Option<String>>>,
    access_token: Arc<Mutex<AccessToken>>,
    refresh_token: Arc<Mutex<RefreshToken>>,
    token_expires: Arc<Mutex<DateTime<Utc>>>,
}

impl FeedlyApi {
    /// Create a new FeedlyApi instance
    /// - `client_id`: identifies the client making requests to feedly
    /// - `client_secret`: issued by feedly staff together with `client_id`
    /// - `access_token`: access token received after sucessfull oauth process
    /// - `refresh_token`: received together with `access_token`, used to refresh it when expired
    /// - `token_expires`: point in time when `access_token` expires and has to refreshed
    pub fn new(
        client_id: String,
        client_secret: String,
        access_token: AccessToken,
        refresh_token: RefreshToken,
        token_expires: DateTime<Utc>,
    ) -> Result<FeedlyApi, ApiError> {
        let api = FeedlyApi {
            base_uri: Self::base_uri()?,
            client_id,
            client_secret,
            user_id: Arc::new(Mutex::new(None)),
            access_token: Arc::new(Mutex::new(access_token)),
            refresh_token: Arc::new(Mutex::new(refresh_token)),
            token_expires: Arc::new(Mutex::new(token_expires)),
        };

        Ok(api)
    }

    /// Generate OAuth login URL.
    pub fn login_url(client_id: &str, client_secret: &str) -> Result<Url, ApiError> {
        let mut url = Self::base_uri()?.as_str().to_owned();
        let auth_scope = Self::auth_scope()?.as_str().to_owned();
        let redirect_url = Self::redirect_uri()?.as_str().to_owned();
        url.push_str("v3/auth/auth");
        url.push_str(&format!("?client_secret={}", client_secret));
        url.push_str(&format!("&client_id={}", client_id));
        url.push_str(&format!("&redirect_uri={}", redirect_url));
        url.push_str(&format!("&scope={}", auth_scope));
        url.push_str("&response_type=code");
        url.push_str("&state=getting_code");
        let url = Url::parse(&url)?;
        Ok(url)
    }

    fn deserialize<T: for<'a> Deserialize<'a>>(json: &str) -> Result<T, ApiError> {
        let result: T = serde_json::from_str(json).map_err(|source| ApiError::Json {
            source,
            json: json.into(),
        })?;
        Ok(result)
    }

    /// Parse the redirected URL after login via OAuth.
    /// Returns the `AuthCode` needed to continue with the login process.
    pub fn parse_redirected_url(url: &Url) -> Result<AuthCode, ApiError> {
        if let Some(code) = url.query_pairs().find(|x| x.0 == "code") {
            return Ok(code.1.to_string());
        }

        if let Some(error) = url.query_pairs().find(|x| x.0 == "error") {
            if error.1 == "access_denied" {
                return Err(ApiError::AccessDenied);
            };
        }

        Err(ApiError::Unknown)
    }

    /// Base URL of the desired redirect URL. Will contain the `AuthCode` and can be fed into `parse_redirected_url`.
    pub fn redirect_uri() -> Result<Url, ApiError> {
        let url = Url::parse("http://localhost")?;
        Ok(url)
    }

    fn auth_scope() -> Result<Url, ApiError> {
        let url = Url::parse("https://cloud.feedly.com/subscriptions")?;
        Ok(url)
    }

    fn base_uri() -> Result<Url, ApiError> {
        let url = Url::parse("https://cloud.feedly.com")?;
        Ok(url)
    }

    /// Will fetch the user id from feedly if not already done so.
    pub async fn initialize_user_id(&self, client: &Client) -> Result<(), ApiError> {
        if self.user_id.lock().await.is_none() {
            let profile = self.get_profile(client).await?;
            self.user_id.lock().await.replace(profile.id);
        }
        Ok(())
    }

    /// Parse expiration date of `access_token` to rust `NaiveDateTime`.
    pub fn parse_expiration_date(expires_in: &str) -> Result<DateTime<Utc>, ApiError> {
        let seconds = expires_in.parse::<i64>().map_err(|_| ApiError::Input)?;
        let now = Utc::now();
        let expires_datetime = now + TimeDelta::try_seconds(seconds).unwrap();
        Ok(expires_datetime)
    }

    /// Generate a feedly feed id from a feed URL (not a website URL).
    pub fn gernerate_feed_id(url: &Url) -> String {
        format!("feed/{}", url.as_str())
    }

    /// Generate a category id based on its name and the user id.
    pub async fn generate_category_id(
        &self,
        title: &str,
        client: &Client,
    ) -> Result<String, ApiError> {
        self.initialize_user_id(client).await?;
        if let Some(user_id) = self.user_id.lock().await.as_deref() {
            return Ok(format!("user/{}/category/{}", user_id, title));
        }
        // should never happen
        Err(ApiError::Unknown)
    }

    /// Generate a tag id based on its name and the user id.
    pub async fn generate_tag_id(&self, title: &str, client: &Client) -> Result<String, ApiError> {
        self.initialize_user_id(client).await?;
        if let Some(user_id) = self.user_id.lock().await.as_deref() {
            return Ok(format!("user/{}/tag/{}", user_id, title));
        }
        // should never happen
        Err(ApiError::Unknown)
    }

    /// The id of the global `all` tag of feedly.
    pub async fn category_all(&self, client: &Client) -> Result<String, ApiError> {
        self.generate_category_id("global.all", client).await
    }

    /// The id of the global `saved` tag of feedly (starred articles).
    pub async fn tag_marked(&self, client: &Client) -> Result<String, ApiError> {
        self.generate_tag_id("global.saved", client).await
    }

    /// The id of the global `read` tag of feedly.
    pub async fn tag_read(&self, client: &Client) -> Result<String, ApiError> {
        self.generate_tag_id("global.read", client).await
    }

    /// Generate a `access_token` from the `AuthCode` parsed from the redirect URL after successful OAuth.
    pub async fn request_auth_token(
        client_id: &str,
        client_secret: &str,
        auth_code: AuthCode,
        client: &Client,
    ) -> Result<AccessTokenResponse, ApiError> {
        let input = json!(
            {
                "code" : auth_code,
                "client_id" : client_id,
                "client_secret" : client_secret,
                "redirect_uri" : Self::redirect_uri()?.as_str(),
                "state" : "feedly-api rust crate",
                "grant_type" : "authorization_code"
            }
        );

        let api_endpoint = Self::base_uri()?.join("/v3/auth/token")?;

        let response = client
            .post(api_endpoint)
            .json(&input)
            .send()
            .await?
            .text()
            .await?;

        let response: AccessTokenResponse = Self::deserialize(&response)?;

        Ok(response)
    }

    /// Refresh the `access_token` after it expired.
    pub async fn refresh_auth_token(
        &self,
        client: &Client,
    ) -> Result<RefreshTokenResponse, ApiError> {
        let refresh_token = self.refresh_token.lock().await.clone();

        let input = json!(
            {
                "refresh_token" : refresh_token,
                "client_id" : self.client_id,
                "client_secret" : self.client_secret,
                "grant_type" : "refresh_token"
            }
        );
        let api_endpoint = self.base_uri.clone().join("/v3/auth/token")?;
        let response = client
            .post(api_endpoint)
            .json(&input)
            .send()
            .await?
            .text()
            .await?;
        let response: RefreshTokenResponse = Self::deserialize(&response)?;

        info!("Feedly refresh token: {:?}", response);

        *self.access_token.lock().await = response.access_token.clone();
        *self.token_expires.lock().await =
            Utc::now() + TimeDelta::try_seconds(response.expires_in as i64).unwrap();

        Ok(response)
    }

    async fn get_access_token(&self) -> Result<AccessToken, ApiError> {
        let expires_at = *self.token_expires.lock().await;

        // check if access_token is still valid
        let expires_in = expires_at.signed_duration_since(Utc::now());
        let expired = expires_in.num_seconds() <= 60;

        if !expired {
            return Ok(self.access_token.lock().await.clone());
        }

        Err(ApiError::TokenExpired)
    }

    async fn post_request<T: Serialize + ?Sized>(
        &self,
        json: &T,
        api_endpoint: &str,
        client: &Client,
    ) -> Result<String, ApiError> {
        let token = self.get_access_token().await?;
        let api_endpoint = self.base_uri.clone().join(api_endpoint)?;
        let response = client
            .post(api_endpoint)
            .header(AUTHORIZATION, token)
            .json(json)
            .send()
            .await?;

        let status = response.status();
        let response = response.text().await?;
        if status != StatusCode::OK {
            let error: FeedlyError = Self::deserialize(&response)?;
            return Err(ApiError::parse_feedly_error(error));
        }
        Ok(response)
    }

    async fn get_request(&self, api_endpoint: &str, client: &Client) -> Result<String, ApiError> {
        let token = self.get_access_token().await?;
        let api_endpoint = self.base_uri.clone().join(api_endpoint)?;
        let response = client
            .get(api_endpoint)
            .header(AUTHORIZATION, token)
            .send()
            .await?;

        let status = response.status();
        let response = response.text().await?;
        if status != StatusCode::OK {
            let error: FeedlyError = Self::deserialize(&response)?;
            return Err(ApiError::parse_feedly_error(error));
        }
        Ok(response)
    }

    async fn put_request<T: Serialize + ?Sized>(
        &self,
        json: &T,
        api_endpoint: &str,
        client: &Client,
    ) -> Result<String, ApiError> {
        let token = self.get_access_token().await?;
        let api_endpoint = self.base_uri.clone().join(api_endpoint)?;
        let response = client
            .put(api_endpoint)
            .header(AUTHORIZATION, token)
            .json(json)
            .send()
            .await?;

        let status = response.status();
        let response = response.text().await?;
        if status != StatusCode::OK {
            let error: FeedlyError = Self::deserialize(&response)?;
            return Err(ApiError::parse_feedly_error(error));
        }
        Ok(response)
    }

    async fn delete_request(&self, api_endpoint: &str, client: &Client) -> Result<(), ApiError> {
        let token = self.get_access_token().await?;
        let api_endpoint = self.base_uri.clone().join(api_endpoint)?;
        let response = client
            .delete(api_endpoint)
            .header(AUTHORIZATION, token)
            .send()
            .await?;
        if response.status() != StatusCode::OK {
            let response = response.text().await?;
            let error: FeedlyError = Self::deserialize(&response)?;
            return Err(ApiError::parse_feedly_error(error));
        }
        Ok(())
    }

    pub async fn get_profile(&self, client: &Client) -> Result<Profile, ApiError> {
        let response = self.get_request("/v3/profile", client).await?;
        let profile: Profile = Self::deserialize(&response)?;
        Ok(profile)
    }

    #[allow(clippy::too_many_arguments)]
    pub async fn update_profile(
        &self,
        client: &Client,
        email: Option<String>,
        given_name: Option<String>,
        family_name: Option<String>,
        picture: Option<String>,
        gender: Option<bool>,
        locale: Option<String>,
        twitter: Option<String>,
        facebook: Option<String>,
    ) -> Result<Profile, ApiError> {
        let update = ProfileUpdate {
            email,
            given_name,
            family_name,
            picture,
            gender,
            locale,
            twitter,
            facebook,
        };

        let response = self.post_request(&update, "/v3/profile", client).await?;
        let profile: Profile = Self::deserialize(&response)?;
        Ok(profile)
    }

    pub async fn get_categories(&self, client: &Client) -> Result<Vec<Category>, ApiError> {
        let response = self
            .get_request("/v3/categories?sort=feedly", client)
            .await?;
        let category_vec: Vec<Category> = Self::deserialize(&response)?;
        Ok(category_vec)
    }

    pub async fn get_collections(&self, client: &Client) -> Result<Vec<Collection>, ApiError> {
        let response = self
            .get_request("/v3/collections?sort=feedly", client)
            .await?;
        let collections_vec: Vec<Collection> = Self::deserialize(&response)?;
        Ok(collections_vec)
    }

    pub async fn get_collection(
        &self,
        id: &str,
        client: &Client,
    ) -> Result<Vec<Collection>, ApiError> {
        let api_endpoint = format!("/v3/collections/{}", id);
        let response = self.get_request(&api_endpoint, client).await?;
        let collection_vec: Vec<Collection> = Self::deserialize(&response)?;
        Ok(collection_vec)
    }

    pub async fn update_category(
        &self,
        id: &str,
        label: &str,
        client: &Client,
    ) -> Result<(), ApiError> {
        let input = json!(
            {
                "label" : label,
            }
        );
        let id = utf8_percent_encode(id, FEEDLY_ENCODE_SET).to_string();
        let endpoint = FeedlyApi::category_api_endpoint(&id);
        let _ = self.post_request(&input, &endpoint, client).await?;
        Ok(())
    }

    pub async fn delete_category(&self, id: &str, client: &Client) -> Result<(), ApiError> {
        let id = utf8_percent_encode(id, FEEDLY_ENCODE_SET).to_string();
        let endpoint = FeedlyApi::category_api_endpoint(&id);
        self.delete_request(&endpoint, client).await?;
        Ok(())
    }

    pub async fn get_subsriptions(&self, client: &Client) -> Result<Vec<Subscription>, ApiError> {
        let response = self.get_request("/v3/subscriptions", client).await?;
        let subscription_vec: Vec<Subscription> = Self::deserialize(&response)?;
        Ok(subscription_vec)
    }

    pub async fn create_or_update_collection(
        &self,
        collection: CollectionInput,
        client: &Client,
    ) -> Result<(), ApiError> {
        let _ = self
            .post_request(&collection, "/v3/collections", client)
            .await?;
        Ok(())
    }

    pub async fn add_feeds_to_collection(
        &self,
        collection_id: &str,
        feeds: Vec<CollectionFeedInput>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_endpoint = format!("/v3/collections/{}/feeds/.mput", collection_id);
        let _ = self.post_request(&feeds, &api_endpoint, client).await?;
        Ok(())
    }

    pub async fn remove_feeds_from_collection(
        &self,
        collection_id: &str,
        feeds: Vec<CollectionFeedInput>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_endpoint = format!("/v3/collections/{}/feeds/.mdelete", collection_id);
        let _ = self.post_request(&feeds, &api_endpoint, client).await?;
        Ok(())
    }

    /// Also updates existing subscriptions.
    pub async fn add_subscription(
        &self,
        subscription: SubscriptionInput,
        client: &Client,
    ) -> Result<(), ApiError> {
        let _ = self
            .post_request(&subscription, "/v3/subscriptions", client)
            .await?;
        Ok(())
    }

    /// Same as `add_subscription` but update multiple subscriptions at once.
    pub async fn update_subscriptions(
        &self,
        subscriptions: Vec<SubscriptionInput>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let _ = self
            .post_request(&subscriptions, "/v3/subscriptions/.mput", client)
            .await?;
        Ok(())
    }

    pub async fn delete_subscription(&self, id: &str, client: &Client) -> Result<(), ApiError> {
        let id = utf8_percent_encode(id, FEEDLY_ENCODE_SET).to_string();
        let api_endpoint = FeedlyApi::subscription_api_endpoint(&id);
        self.delete_request(&api_endpoint, client).await?;
        Ok(())
    }

    pub async fn get_tags(&self, client: &Client) -> Result<Vec<Tag>, ApiError> {
        let response = self.get_request("/v3/tags", client).await?;
        Self::deserialize(&response)
    }

    fn category_api_endpoint(category_id: &str) -> String {
        let mut api_endpoint = String::from("/v3/categories/");
        api_endpoint.push_str(category_id);
        api_endpoint
    }

    fn subscription_api_endpoint(subscription_id: &str) -> String {
        let mut api_endpoint = String::from("/v3/subscriptions/");
        api_endpoint.push_str(subscription_id);
        api_endpoint
    }

    fn tag_api_endpoint(
        tag_ids: Vec<&str>,
        entry_ids: Option<Vec<&str>>,
    ) -> Result<String, ApiError> {
        if tag_ids.is_empty() {
            return Err(ApiError::Input);
        }

        let mut api_endpoint = String::from("/v3/tags/");
        for tag_id in tag_ids {
            let tag_id = utf8_percent_encode(tag_id, FEEDLY_ENCODE_SET).to_string();
            api_endpoint.push_str(&tag_id);
            api_endpoint.push(',');
        }
        api_endpoint = api_endpoint[..api_endpoint.len() - 1].to_owned();
        if let Some(entry_ids) = entry_ids {
            if entry_ids.is_empty() {
                return Err(ApiError::Input);
            }
            api_endpoint.push('/');
            for entry_id in entry_ids {
                let entry_id = utf8_percent_encode(entry_id, FEEDLY_ENCODE_SET).to_string();
                api_endpoint.push_str(&entry_id);
                api_endpoint.push(',');
            }
            api_endpoint = api_endpoint[..api_endpoint.len() - 1].to_owned();
        }

        Ok(api_endpoint)
    }

    /// Add a tag to an entry (also used to mark entries as read or starred etc.).
    pub async fn tag_entry(
        &self,
        entry_id: &str,
        tag_ids: Vec<&str>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let json = json!(
            {
                "entryId" : entry_id,
            }
        );
        let api_endpoint = FeedlyApi::tag_api_endpoint(tag_ids, None)?;
        let _ = self.put_request(&json, &api_endpoint, client).await?;
        Ok(())
    }

    /// Same as `tag_entry` but for multiple entries at once.
    pub async fn tag_entries(
        &self,
        entry_ids: Vec<&str>,
        tag_ids: Vec<&str>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let json = json!(
            {
                "entryIds" : entry_ids,
            }
        );
        let api_endpoint = FeedlyApi::tag_api_endpoint(tag_ids, None)?;
        let _ = self.put_request(&json, &api_endpoint, client).await?;
        Ok(())
    }

    pub async fn update_tag(
        &self,
        tag_id: &str,
        label: &str,
        client: &Client,
    ) -> Result<(), ApiError> {
        let json = json!(
            {
                "label" : label,
            }
        );
        let api_endpoint = FeedlyApi::tag_api_endpoint(vec![tag_id], None)?;
        let _ = self.post_request(&json, &api_endpoint, client).await?;
        Ok(())
    }

    pub async fn untag_entries(
        &self,
        entry_ids: Vec<&str>,
        tag_ids: Vec<&str>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_endpoint = FeedlyApi::tag_api_endpoint(tag_ids, Some(entry_ids))?;
        self.delete_request(&api_endpoint, client).await?;
        Ok(())
    }

    pub async fn delete_tags(&self, tag_ids: Vec<&str>, client: &Client) -> Result<(), ApiError> {
        let api_endpoint = FeedlyApi::tag_api_endpoint(tag_ids, None)?;
        self.delete_request(&api_endpoint, client).await?;
        Ok(())
    }

    pub async fn get_entries(
        &self,
        entry_ids: Vec<&str>,
        client: &Client,
    ) -> Result<Vec<Entry>, ApiError> {
        let response = self
            .post_request(&entry_ids, "/v3/entries/.mget", client)
            .await?;
        match Self::deserialize(&response) {
            Ok(entries) => Ok(entries),
            Err(error) => {
                log::warn!("Failed to deserialize entries to struct: {}", error);
                log::debug!("Trying to deserialize response manually");
                if let Ok(entries_value) = serde_json::from_str::<serde_json::Value>(&response) {
                    Entry::manual_deserialize_vec(&entries_value)
                } else {
                    Err(error)
                }
            }
        }
    }

    pub async fn create_entry(
        &self,
        entry: Entry,
        client: &Client,
    ) -> Result<Vec<String>, ApiError> {
        let response = self.post_request(&entry, "/v3/entries/", client).await?;
        Self::deserialize(&response)
    }

    fn stream_api_endpoint(
        stream_id: &str,
        continuation: Option<String>,
        count: Option<u32>,
        ranked: Option<&str>,
        unread_only: Option<bool>,
        newer_than: Option<u64>,
    ) -> String {
        let mut api_endpoint = String::from("/v3/streams/contents?streamId=");
        let stream_id = utf8_percent_encode(stream_id, FEEDLY_ENCODE_SET).to_string();
        api_endpoint.push_str(&stream_id);

        if let Some(continuation) = continuation {
            api_endpoint.push_str(&format!("&continuation={}", continuation));
        }

        if let Some(count) = count {
            api_endpoint.push_str(&format!("&count={}", count));
        }

        if let Some(ranked) = ranked {
            api_endpoint.push_str(&format!("&ranked={}", ranked));
        }

        if let Some(unread_only) = unread_only {
            api_endpoint.push_str(&format!("&unreadOnly={}", unread_only));
        }

        if let Some(newer_than) = newer_than {
            api_endpoint.push_str(&format!("&newerThan={}", newer_than));
        }

        api_endpoint
    }

    #[allow(clippy::too_many_arguments)]
    pub async fn get_stream(
        &self,
        stream_id: &str,
        continuation: Option<String>,
        count: Option<u32>,
        ranked: Option<&str>,
        unread_only: Option<bool>,
        newer_than: Option<u64>,
        client: &Client,
    ) -> Result<Stream, ApiError> {
        let api_endpoint = FeedlyApi::stream_api_endpoint(
            stream_id,
            continuation,
            count,
            ranked,
            unread_only,
            newer_than,
        );
        let response = self.get_request(&api_endpoint, client).await?;
        let stream: Stream = match serde_json::from_str(&response) {
            Ok(stream) => stream,
            Err(error) => {
                log::warn!("Failed to deserialize stream to struct: {}", error);
                log::debug!("Trying to deserialize stream manually");
                Stream::manual_deserialize(&response)?
            }
        };
        Ok(stream)
    }

    pub async fn get_unread_counts(&self, client: &Client) -> Result<Counts, ApiError> {
        let response = self.get_request("/v3/markers/counts", client).await?;
        Self::deserialize(&response)
    }

    pub async fn mark_entries_read(
        &self,
        entry_ids: Vec<&str>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let json = json!(
            {
                "action" : "markAsRead",
                "type" : "entries",
                "entryIds" : entry_ids
            }
        );
        let _ = self.post_request(&json, "/v3/markers", client).await?;
        Ok(())
    }

    pub async fn mark_entries_unread(
        &self,
        entry_ids: Vec<&str>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let json = json!(
            {
                "action" : "keepUnread",
                "type" : "entries",
                "entryIds" : entry_ids
            }
        );
        let _ = self.post_request(&json, "/v3/markers", client).await?;
        Ok(())
    }

    pub async fn mark_feeds_read(
        &self,
        feed_ids: Vec<&str>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let json = json!(
            {
                "action" : "markAsRead",
                "type" : "feeds",
                "feedIds" : feed_ids
            }
        );
        let _ = self.post_request(&json, "/v3/markers", client).await?;
        Ok(())
    }

    pub async fn mark_categories_read(
        &self,
        category_ids: Vec<&str>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let json = json!(
            {
                "action" : "markAsRead",
                "type" : "categories",
                "categoryIds" : category_ids
            }
        );
        let _ = self.post_request(&json, "/v3/markers", client).await?;
        Ok(())
    }

    pub async fn mark_tags_read(
        &self,
        tag_ids: Vec<&str>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let json = json!(
            {
                "action" : "markAsRead",
                "type" : "tags",
                "tagIds" : tag_ids
            }
        );
        let _ = self.post_request(&json, "/v3/markers", client).await?;
        Ok(())
    }

    pub async fn mark_entries_saved(
        &self,
        entry_ids: Vec<&str>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let json = json!(
            {
                "action" : "markAsSaved",
                "type" : "entries",
                "entryIds" : entry_ids
            }
        );
        let _ = self.post_request(&json, "/v3/markers", client).await?;
        Ok(())
    }

    pub async fn mark_entries_unsaved(
        &self,
        entry_ids: Vec<&str>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let json = json!(
            {
                "action" : "markAsUnsaved",
                "type" : "entries",
                "entryIds" : entry_ids
            }
        );
        let _ = self.post_request(&json, "/v3/markers", client).await?;
        Ok(())
    }

    #[allow(dead_code)]
    pub async fn export_opml(&self, client: &Client) -> Result<String, ApiError> {
        self.get_request("/v3/opml", client).await
    }

    pub async fn import_opml(&self, opml: &str, client: &Client) -> Result<(), ApiError> {
        let token = self.get_access_token().await?;
        let api_endpoint = self.base_uri.clone().join("/v3/opml")?;
        let response = client
            .post(api_endpoint)
            .header(AUTHORIZATION, token)
            .header(CONTENT_TYPE, "text/xml")
            .body(opml.to_owned())
            .send()
            .await?;

        let status = response.status();
        let response = response.text().await?;
        if status != StatusCode::OK {
            let error: FeedlyError = Self::deserialize(&response)?;
            return Err(ApiError::parse_feedly_error(error));
        }
        Ok(())
    }

    /// Search the feedly cloud for new feeds to subscribe to
    ///
    ///  # Arguments
    ///
    /// * `client` - reqwest::Client
    /// * `query` - Can be a feed url, a site title, a site url or a #topic.
    /// * `count` - Number of results. Default value is 20.
    /// * `locale` - Locale hint for the search engine to return feeds in that localse (e.g. "pt", "fr_FR").
    pub async fn search_feedly_cloud(
        client: &Client,
        query: &str,
        count: Option<u32>,
        locale: Option<&str>,
    ) -> Result<SearchResult, ApiError> {
        let mut query = format!(
            "/v3/search/feeds?query={}",
            utf8_percent_encode(query, FEEDLY_ENCODE_SET)
        );

        if let Some(count) = count {
            query.push_str(&format!("&count={}", count));
        }

        if let Some(locale) = locale {
            let locale = utf8_percent_encode(locale, FEEDLY_ENCODE_SET).to_string();
            query.push_str(&format!("&locale={}", locale));
        }

        let api_endpoint = Self::base_uri()?.join(&query)?;

        let response = client.get(api_endpoint).send().await?;

        let status = response.status();
        let response = response.text().await?;

        if status != StatusCode::OK {
            let error: FeedlyError = Self::deserialize(&response)?;
            return Err(ApiError::parse_feedly_error(error));
        }

        Self::deserialize(&response)
    }
}
