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

pub use crate::error::ApiError;
use reqwest::{Client, header::COOKIE, multipart};
use serde_json::{self, Value};
use url::Url;

pub struct NewsBlurApi {
    base_uri: Url,
    username: String,
    password: String,
    cookie: Option<String>,
}

impl NewsBlurApi {
    /// Create a new instance of the NewsBlurApi
    pub fn new(url: &Url, username: &str, password: &str, cookie: Option<String>) -> Self {
        NewsBlurApi {
            base_uri: url.clone(),
            username: username.to_string(),
            password: password.to_string(),
            cookie,
        }
    }

    fn cookie(&self) -> Result<&str, ApiError> {
        self.cookie.as_deref().ok_or(ApiError::AccessDenied)
    }

    /// Login to NewsBlur. This must be called before other functions
    ///
    /// On success returns the cookie used for login
    pub async fn login(&mut self, client: &Client) -> Result<String, ApiError> {
        let form = multipart::Form::new()
            .text("username", self.username.clone())
            .text("password", self.password.clone());

        let api_url: Url = self.base_uri.join("api/login")?;

        let response = client.post(api_url).multipart(form).send().await?;

        let status = response.status();

        if !status.is_success() {
            return Err(ApiError::AccessDenied);
        }

        let cookie = response.cookies().next().ok_or(ApiError::AccessDenied)?;
        let cookie_string = format!("{}={}", cookie.name(), cookie.value());
        self.cookie = Some(cookie_string.clone());
        Ok(cookie_string)
    }

    /// Logout of NewsBlur
    pub async fn logout(&self, client: &Client) -> Result<(), ApiError> {
        let api_url: Url = self.base_uri.join("api/logout")?;
        let cookie = self.cookie()?;

        let response = client.post(api_url).header(COOKIE, cookie).send().await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        Ok(())
    }

    /// Sign up to NewsBlur
    pub async fn signup(&self, _client: &Client) -> Result<(), ApiError> {
        unimplemented!()
    }

    /// Retrieve information about a feed from its website or RSS address.
    pub async fn search_feed(&self, client: &Client, address: &str) -> Result<(), ApiError> {
        let form = multipart::Form::new().text("address", address.to_string());
        let api_url: Url = self.base_uri.join("rss_feeds/search_feed")?;
        let cookie = self.cookie()?;

        let response = client
            .get(api_url)
            .header(COOKIE, cookie)
            .multipart(form)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        Ok(())
    }

    /// Retrieve a list of feeds to which a user is actively subscribed.
    pub async fn get_feeds(&self, client: &Client) -> Result<Value, ApiError> {
        let form = multipart::Form::new().text("include_favicons", "false");
        let api_url: Url = self.base_uri.join("reader/feeds")?;
        let cookie = self.cookie()?;

        let response = client
            .get(api_url)
            .header(COOKIE, cookie)
            .multipart(form)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        let response_json = response.json().await?;

        Ok(response_json)
    }

    /// Retrieve a list of favicons for a list of feeds. Used when combined
    /// with /reader/feeds and include_favicons=false, so the feeds request
    /// contains far less data. Useful for mobile devices, but requires a
    /// second request.
    pub async fn favicons(&self, client: &Client, feed_id: &str) -> Result<Value, ApiError> {
        let form = multipart::Form::new().text("feed_ids", feed_id.to_string());
        let api_url: Url = self.base_uri.join("reader/favicons")?;
        let cookie = self.cookie()?;

        let response = client
            .get(api_url)
            .header(COOKIE, cookie)
            .multipart(form)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        let response_json = response.json().await?;

        Ok(response_json)
    }

    /// Retrieve the original page from a single feed.
    pub async fn get_original_page(&self, client: &Client, id: &str) -> Result<String, ApiError> {
        let request = format!("reader/page/{id}");
        let api_url: Url = self.base_uri.join(&request)?;
        let cookie = self.cookie()?;

        let response = client.get(api_url).header(COOKIE, cookie).send().await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

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

        Ok(response_text)
    }

    /// Retrieve the original page from a single feed.
    pub async fn get_original_text(&self, client: &Client, id: &str) -> Result<String, ApiError> {
        let api_url: Url = self.base_uri.join("rss_feeds/original_text")?;
        let query = vec![("story_hash", id.to_string())];
        let cookie = self.cookie()?;

        let response = client
            .get(api_url)
            .header(COOKIE, cookie)
            .query(&query)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

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

        Ok(response_text)
    }

    /// Up-to-the-second unread counts for each active feed.
    /// Poll for these counts no more than once a minute.
    pub async fn refresh_feeds(&self, client: &Client) -> Result<Value, ApiError> {
        let api_url: Url = self.base_uri.join("reader/refresh_feeds")?;
        let cookie = self.cookie()?;

        let response = client.get(api_url).header(COOKIE, cookie).send().await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        let response_json = response.json().await?;

        Ok(response_json)
    }

    /// Feed of previously read stories.
    pub async fn get_read_stories(&self, client: &Client, page: u32) -> Result<Value, ApiError> {
        let query = vec![("page", format!("{page}"))];
        let cookie = self.cookie()?;

        let api_url: Url = self.base_uri.join("reader/read_stories")?;

        let response = client
            .get(api_url)
            .header(COOKIE, cookie)
            .query(&query)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        let response_json = response.json().await?;

        Ok(response_json)
    }

    /// Retrieve stories from a single feed.
    pub async fn get_stories(
        &self,
        client: &Client,
        id: &str,
        include_content: bool,
        page: u32,
    ) -> Result<Value, ApiError> {
        let request = format!("reader/feed/{id}");
        let cookie = self.cookie()?;
        let mut query = Vec::new();

        if include_content {
            query.push(("include_content", "true".to_string()));
        } else {
            query.push(("include_content", "false".to_string()));
        }
        query.push(("page", format!("{page}")));

        let api_url: Url = self.base_uri.join(&request)?;

        let response = client
            .get(api_url)
            .header(COOKIE, cookie)
            .query(&query)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        let response_json = response.json().await?;

        Ok(response_json)
    }

    /// Mark stories as read using their unique story_hash.
    pub async fn mark_stories_read(
        &self,
        client: &Client,
        story_hashes: &[&str],
    ) -> Result<(), ApiError> {
        let mut form = multipart::Form::new();
        for story_hash in story_hashes {
            form = form.text("story_hash", story_hash.to_string());
        }

        let api_url: Url = self.base_uri.join("reader/mark_story_hashes_as_read")?;
        let cookie = self.cookie()?;

        let response = client
            .post(api_url)
            .header(COOKIE, cookie)
            .multipart(form)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        Ok(())
    }

    /// Mark a single story as unread using its unique story_hash.
    pub async fn mark_story_unread(
        &self,
        client: &Client,
        story_hashes: &[&str],
    ) -> Result<(), ApiError> {
        let mut form = multipart::Form::new();
        for story_hash in story_hashes {
            form = form.text("story_hash", story_hash.to_string());
        }

        let api_url: Url = self.base_uri.join("reader/mark_story_hash_as_unread")?;
        let cookie = self.cookie()?;

        let response = client
            .post(api_url)
            .header(COOKIE, cookie)
            .multipart(form)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        Ok(())
    }

    /// Mark a story as starred (saved).
    pub async fn mark_story_hash_as_starred(
        &self,
        client: &Client,
        story_hash: &str,
    ) -> Result<(), ApiError> {
        let form = multipart::Form::new().text("story_hash", story_hash.to_string());

        let api_url: Url = self.base_uri.join("reader/mark_story_hash_as_starred")?;
        let cookie = self.cookie()?;

        let response = client
            .post(api_url)
            .header(COOKIE, cookie)
            .multipart(form)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        Ok(())
    }

    /// Mark a story as unstarred (unsaved).
    pub async fn mark_story_hash_as_unstarred(
        &self,
        client: &Client,
        story_hash: &str,
    ) -> Result<(), ApiError> {
        let form = multipart::Form::new().text("story_hash", story_hash.to_string());

        let api_url: Url = self.base_uri.join("reader/mark_story_hash_as_unstarred")?;
        let cookie = self.cookie()?;

        let response = client
            .post(api_url)
            .header(COOKIE, cookie)
            .multipart(form)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        Ok(())
    }

    /// The story_hashes of all unread stories.
    /// Useful for offline access of stories and quick unread syncing.
    /// Use include_timestamps to fetch stories in date order.
    pub async fn get_unread_story_hashes(&self, client: &Client) -> Result<Value, ApiError> {
        let api_url: Url = self.base_uri.join("reader/unread_story_hashes")?;
        let cookie = self.cookie()?;

        let response = client.get(api_url).header(COOKIE, cookie).send().await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        let response_json = response.json().await?;

        Ok(response_json)
    }

    pub async fn get_stared_story_hashes(&self, client: &Client) -> Result<Value, ApiError> {
        let api_url: Url = self.base_uri.join("reader/starred_story_hashes")?;
        let cookie = self.cookie()?;

        let response = client.get(api_url).header(COOKIE, cookie).send().await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        let response_json = response.json().await?;

        Ok(response_json)
    }

    /// Retrieve up to 100 stories when specifying by story_hash.
    pub async fn get_river_stories(
        &self,
        client: &Client,
        hashes: &[&str],
    ) -> Result<Value, ApiError> {
        let api_url: Url = self.base_uri.join("reader/river_stories")?;
        let cookie = self.cookie()?;

        let mut query = Vec::new();
        for hash in hashes {
            query.push(("h", hash));
        }

        let response = client
            .get(api_url)
            .header(COOKIE, cookie)
            .query(&query)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        let response_json = response.json().await?;

        Ok(response_json)
    }

    pub async fn mark_feed_read(&self, client: &Client, feed_id: &str) -> Result<(), ApiError> {
        let form = multipart::Form::new().text("feed_id", feed_id.to_string());

        let api_url: Url = self.base_uri.join("reader/mark_feed_as_read")?;
        let cookie = self.cookie()?;

        let response = client
            .post(api_url)
            .header(COOKIE, cookie)
            .multipart(form)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        Ok(())
    }

    pub async fn mark_all_read(&self, client: &Client) -> Result<(), ApiError> {
        let api_url: Url = self.base_uri.join("reader/mark_all_as_read")?;
        let cookie = self.cookie()?;

        let response = client.post(api_url).header(COOKIE, cookie).send().await?;

        if !response.status().is_success() {
            return Err(ApiError::AccessDenied);
        }

        Ok(())
    }
}
