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

pub use crate::error::ApiError;

use crate::models::{
    Category, CategoryInput, Entry, EntryBatch, EntryStateUpdate, EntryStatus, FavIcon, Feed,
    FeedCreation, FeedCreationResponse, FeedDiscovery, FeedModification, MinifluxError, OrderBy,
    OrderDirection, User, UserCreation, UserModification,
};
use base64::engine::general_purpose::STANDARD as base64_std;
use base64::Engine;
use log::error;
use reqwest::{header::AUTHORIZATION, Client, StatusCode};
use serde::{Deserialize, Serialize};
use url::Url;

type FeedID = i64;
type CategoryID = i64;
type EntryID = i64;
type UserID = i64;
type IconID = i64;
type EnclosureID = i64;

enum ApiAuth {
    Basic(String),
    Token(String),
}

pub struct MinifluxApi {
    base_uri: Url,
    auth: ApiAuth,
}

impl MinifluxApi {
    /// Create a new instance of the MinifluxApi.
    /// - `url`: url of the hosted Miniflux instance (e.g. `https://reader.miniflux.app/`)
    /// - `username`: user existing on said Miniflux instance
    /// - `password`: password of said user
    pub fn new(url: &Url, username: String, password: String) -> Self {
        MinifluxApi {
            base_uri: url.clone(),
            auth: ApiAuth::Basic(Self::generate_basic_auth(&username, &password)),
        }
    }

    /// Create a new instance of the MinifluxApi using Token Auth.
    /// - `url`: url of the hosted Miniflux instance (e.g. `https://reader.miniflux.app/`)
    /// - `token`: token generated by Miniflux instance
    pub fn new_from_token(url: &Url, token: String) -> Self {
        MinifluxApi {
            base_uri: url.clone(),
            auth: ApiAuth::Token(token),
        }
    }

    fn generate_basic_auth(username: &str, password: &str) -> String {
        let auth = format!("{}:{}", username, password);
        let auth = base64_std.encode(auth);
        format!("Basic {}", auth)
    }

    async fn send_request<T: Serialize>(
        &self,
        client: reqwest::RequestBuilder,
        json_content: Option<T>,
    ) -> Result<reqwest::Response, ApiError> {
        let mut headered_client = match &self.auth {
            ApiAuth::Basic(auth) => client.header(AUTHORIZATION, auth.clone()),
            ApiAuth::Token(auth) => client.header("X-Auth-Token", auth.clone()),
        };
        if let Some(json_content) = json_content {
            headered_client = headered_client.json(&json_content);
        }
        let response = headered_client.send().await?;
        Ok(response)
    }

    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)
    }

    async fn parse_error(
        response: reqwest::Response,
        expected_http: StatusCode,
    ) -> Result<String, ApiError> {
        let status = response.status();
        let response = response.text().await?;
        if status != expected_http {
            let error = Self::deserialize::<MinifluxError>(&response)?;
            error!("Miniflux API: {}", error.error_message);
            return Err(ApiError::Miniflux(error));
        }
        Ok(response)
    }

    /// Try to find all available feeds (RSS/Atom) for a given website url.
    /// - `url`: url of a website with possible feeds (e.g. `http://example.org`)
    pub async fn discover_subscription(
        &self,
        url: Url,
        client: &Client,
    ) -> Result<Vec<Feed>, ApiError> {
        let api_url = self.base_uri.clone().join("v1/discover")?;
        let content = FeedDiscovery {
            url: url.to_string(),
        };
        let response = self
            .send_request(client.post(api_url), Some(content))
            .await?;

        let response = Self::parse_error(response, StatusCode::OK).await?;
        let feeds = Self::deserialize::<Vec<Feed>>(&response)?;
        Ok(feeds)
    }

    /// Get all subscribed feeds.
    pub async fn get_feeds(&self, client: &Client) -> Result<Vec<Feed>, ApiError> {
        let api_url = self.base_uri.clone().join("v1/feeds")?;
        let response = self.send_request::<()>(client.get(api_url), None).await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        let feeds = Self::deserialize::<Vec<Feed>>(&response)?;
        Ok(feeds)
    }

    /// Get a specific feed by id.
    pub async fn get_feed(&self, id: FeedID, client: &Client) -> Result<Feed, ApiError> {
        let api_url = self.base_uri.clone().join(&format!("v1/feeds/{}", id))?;

        let response = self.send_request::<()>(client.get(api_url), None).await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        let feed = Self::deserialize::<Feed>(&response)?;
        Ok(feed)
    }

    /// Get the FavIcon for a specific feed.
    pub async fn get_feed_icon(&self, id: FeedID, client: &Client) -> Result<FavIcon, ApiError> {
        let api_url = self
            .base_uri
            .clone()
            .join(&format!("v1/feeds/{}/icon", id))?;
        let response = self.send_request::<()>(client.get(api_url), None).await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        let icon = Self::deserialize::<FavIcon>(&response)?;
        Ok(icon)
    }

    /// Subscribe to a feed.
    /// - `feed_url`: url to a RSS or Atom feed (e.g. `http://example.org/feed.atom`)
    /// - `category_id`: Miniflux internal id of a category the feed should be created in
    pub async fn create_feed(
        &self,
        feed_url: &Url,
        category_id: CategoryID,
        client: &Client,
    ) -> Result<FeedID, ApiError> {
        let api_url = self.base_uri.clone().join("v1/feeds")?;
        let content = FeedCreation {
            feed_url: feed_url.to_string(),
            category_id,
        };
        let response = self
            .send_request(client.post(api_url), Some(content))
            .await?;

        let response = Self::parse_error(response, StatusCode::CREATED).await?;
        let response = Self::deserialize::<FeedCreationResponse>(&response)?;
        Ok(response.feed_id)
    }

    /// Update title and/or move feed to a different category.
    /// - `id`: Miniflux internal id of the feed to alter
    /// - `title`: new title of the feed
    /// - `category_id`: new parent category id
    #[allow(clippy::too_many_arguments)]
    pub async fn update_feed(
        &self,
        id: FeedID,
        title: Option<&str>,
        category_id: Option<CategoryID>,
        feed_url: Option<&str>,
        site_url: Option<&str>,
        username: Option<&str>,
        password: Option<&str>,
        user_agent: Option<&str>,
        client: &Client,
    ) -> Result<Feed, ApiError> {
        let api_url = self.base_uri.clone().join(&format!("v1/feeds/{}", id))?;
        let content = FeedModification {
            title: title.map(|t| t.into()),
            category_id,
            feed_url: feed_url.map(|t| t.into()),
            site_url: site_url.map(|t| t.into()),
            username: username.map(|t| t.into()),
            password: password.map(|t| t.into()),
            scraper_rules: None,
            rewrite_rules: None,
            crawler: None,
            user_agent: user_agent.map(|t| t.into()),
            disabled: None,
        };
        let response = self
            .send_request(client.put(api_url), Some(content))
            .await?;
        let response = Self::parse_error(response, StatusCode::CREATED).await?;
        let feed = Self::deserialize::<Feed>(&response)?;
        Ok(feed)
    }

    /// Refresh the contents of a feed synchronous on Miniflux.
    /// This operation can block the Miniflux instance for hundrets of milliseconds.
    pub async fn refresh_feed_synchronous(
        &self,
        id: FeedID,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_url = self
            .base_uri
            .clone()
            .join(&format!("v1/feeds/{}/refresh", id))?;
        let response = self.send_request::<()>(client.put(api_url), None).await?;
        let _ = Self::parse_error(response, StatusCode::NO_CONTENT).await?;
        Ok(())
    }

    /// Unsubscribe from a feed.
    pub async fn delete_feed(&self, id: FeedID, client: &Client) -> Result<(), ApiError> {
        let api_url = self.base_uri.clone().join(&format!("v1/feeds/{}", id))?;
        let response = self
            .send_request::<()>(client.delete(api_url), None)
            .await?;
        let _ = Self::parse_error(response, StatusCode::NO_CONTENT).await?;
        Ok(())
    }

    /// Get a single specific entry (= article) from a feed.
    pub async fn get_feed_entry(
        &self,
        feed_id: FeedID,
        entry_id: EntryID,
        client: &Client,
    ) -> Result<Entry, ApiError> {
        let api_url = self
            .base_uri
            .clone()
            .join(&format!("v1/feeds/{}/entries/{}", feed_id, entry_id))?;
        let response = self.send_request::<()>(client.get(api_url), None).await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        let entry = Self::deserialize::<Entry>(&response)?;
        Ok(entry)
    }

    /// Get a single specific entry (= article).
    pub async fn get_entry(&self, id: EntryID, client: &Client) -> Result<Entry, ApiError> {
        let api_url = self.base_uri.clone().join(&format!("v1/entries/{}", id))?;
        let response = self.send_request::<()>(client.get(api_url), None).await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        let entry = Self::deserialize::<Entry>(&response)?;
        Ok(entry)
    }

    /// Get a batch of entries (= articles).
    #[allow(clippy::too_many_arguments)]
    pub async fn get_entries(
        &self,
        status: Option<EntryStatus>,
        offset: Option<i64>,
        limit: Option<i64>,
        order: Option<OrderBy>,
        direction: Option<OrderDirection>,
        before: Option<i64>,
        after: Option<i64>,
        before_entry_id: Option<EntryID>,
        after_entry_id: Option<EntryID>,
        starred: Option<bool>,
        client: &Client,
    ) -> Result<Vec<Entry>, ApiError> {
        let mut api_url = self.base_uri.clone().join("v1/entries")?;
        {
            let mut query_pairs = api_url.query_pairs_mut();
            query_pairs.clear();

            if let Some(status) = status {
                query_pairs.append_pair("status", status.into());
            }

            if let Some(offset) = offset {
                query_pairs.append_pair("offset", &offset.to_string());
            }

            if let Some(limit) = limit {
                query_pairs.append_pair("limit", &limit.to_string());
            }

            if let Some(order) = order {
                query_pairs.append_pair("order", order.into());
            }

            if let Some(direction) = direction {
                query_pairs.append_pair("direction", direction.into());
            }

            if let Some(before) = before {
                query_pairs.append_pair("before", &before.to_string());
            }

            if let Some(after) = after {
                query_pairs.append_pair("after", &after.to_string());
            }

            if let Some(before_entry_id) = before_entry_id {
                query_pairs.append_pair("before_entry_id", &before_entry_id.to_string());
            }

            if let Some(after_entry_id) = after_entry_id {
                query_pairs.append_pair("after_entry_id", &after_entry_id.to_string());
            }

            if let Some(starred) = starred {
                query_pairs.append_pair("starred", &starred.to_string());
            }
        }

        let response = self.send_request::<()>(client.get(api_url), None).await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        let batch = Self::deserialize::<EntryBatch>(&response)?;
        Ok(batch.entries)
    }

    /// Get a batch of entries (= articles) from a specific feed.
    /// The field comments_url is available since Miniflux v2.0.5.
    #[allow(clippy::too_many_arguments)]
    pub async fn get_feed_entries(
        &self,
        id: FeedID,
        status: Option<EntryStatus>,
        offset: Option<i64>,
        limit: Option<i64>,
        order: Option<OrderBy>,
        direction: Option<OrderDirection>,
        before: Option<i64>,
        after: Option<i64>,
        before_entry_id: Option<EntryID>,
        after_entry_id: Option<EntryID>,
        starred: Option<bool>,
        client: &Client,
    ) -> Result<Vec<Entry>, ApiError> {
        let mut api_url = self
            .base_uri
            .clone()
            .join(&format!("v1/feeds/{}/entries", id))?;
        {
            let mut query_pairs = api_url.query_pairs_mut();
            query_pairs.clear();

            if let Some(status) = status {
                query_pairs.append_pair("status", status.into());
            }

            if let Some(offset) = offset {
                query_pairs.append_pair("offset", &offset.to_string());
            }

            if let Some(limit) = limit {
                query_pairs.append_pair("limit", &limit.to_string());
            }

            if let Some(order) = order {
                query_pairs.append_pair("order", order.into());
            }

            if let Some(direction) = direction {
                query_pairs.append_pair("direction", direction.into());
            }

            if let Some(before) = before {
                query_pairs.append_pair("before", &before.to_string());
            }

            if let Some(after) = after {
                query_pairs.append_pair("after", &after.to_string());
            }

            if let Some(before_entry_id) = before_entry_id {
                query_pairs.append_pair("before_entry_id", &before_entry_id.to_string());
            }

            if let Some(after_entry_id) = after_entry_id {
                query_pairs.append_pair("after_entry_id", &after_entry_id.to_string());
            }

            if let Some(starred) = starred {
                query_pairs.append_pair("starred", &starred.to_string());
            }
        }

        let response = self.send_request::<()>(client.get(api_url), None).await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        let batch = Self::deserialize::<EntryBatch>(&response)?;
        Ok(batch.entries)
    }

    /// Update the read status of a batch of entries (= articles).
    pub async fn update_entries_status(
        &self,
        ids: Vec<FeedID>,
        status: EntryStatus,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_url = self.base_uri.clone().join("v1/entries")?;
        let status: &str = status.into();
        let content = EntryStateUpdate {
            entry_ids: ids,
            status: status.to_owned(),
        };
        let response = self
            .send_request(client.put(api_url), Some(content))
            .await?;
        let _ = Self::parse_error(response, StatusCode::NO_CONTENT).await?;
        Ok(())
    }

    /// Toggle the starred status of an entry (= article)
    pub async fn toggle_bookmark(&self, id: EntryID, client: &Client) -> Result<(), ApiError> {
        let api_url = self
            .base_uri
            .clone()
            .join(&format!("v1/entries/{}/bookmark", id))?;
        let response = self.send_request::<()>(client.put(api_url), None).await?;
        let _ = Self::parse_error(response, StatusCode::NO_CONTENT).await?;
        Ok(())
    }

    /// Get all categories
    pub async fn get_categories(&self, client: &Client) -> Result<Vec<Category>, ApiError> {
        let api_url = self.base_uri.clone().join("v1/categories")?;
        let response = self.send_request::<()>(client.get(api_url), None).await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        let categories = Self::deserialize::<Vec<Category>>(&response)?;
        Ok(categories)
    }

    /// Create a new empty category
    pub async fn create_category(
        &self,
        title: &str,
        client: &Client,
    ) -> Result<Category, ApiError> {
        let api_url = self.base_uri.clone().join("v1/categories")?;
        let content = CategoryInput {
            title: title.to_owned(),
        };
        let response = self
            .send_request(client.post(api_url), Some(content))
            .await?;
        let response = Self::parse_error(response, StatusCode::CREATED).await?;
        let category = Self::deserialize::<Category>(&response)?;
        Ok(category)
    }

    /// Rename a existing cagegory
    pub async fn update_category(
        &self,
        id: CategoryID,
        title: &str,
        client: &Client,
    ) -> Result<Category, ApiError> {
        let api_url = self
            .base_uri
            .clone()
            .join(&format!("v1/categories/{}", id))?;
        let content = CategoryInput {
            title: title.to_owned(),
        };
        let response = self
            .send_request(client.put(api_url), Some(content))
            .await?;
        let response = Self::parse_error(response, StatusCode::CREATED).await?;
        let category = Self::deserialize::<Category>(&response)?;
        Ok(category)
    }

    /// Delete a existing category
    pub async fn delete_category(&self, id: CategoryID, client: &Client) -> Result<(), ApiError> {
        let api_url = self
            .base_uri
            .clone()
            .join(&format!("v1/categories/{}", id))?;
        let response = self
            .send_request::<()>(client.delete(api_url), None)
            .await?;
        let _ = Self::parse_error(response, StatusCode::NO_CONTENT).await?;
        Ok(())
    }

    /// Serialize all categories and subscribed feeds into a OPML string.
    /// This API call is available since Miniflux v2.0.1.
    pub async fn export_opml(&self, client: &Client) -> Result<String, ApiError> {
        let api_url = self.base_uri.clone().join("v1/export")?;
        let response = self.send_request::<()>(client.get(api_url), None).await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        Ok(response)
    }

    /// Parse OPML string, create all contained categories and subscribe to all contained feeds.
    /// This API call is available since Miniflux v2.0.7.
    pub async fn import_opml(&self, opml: &str, client: &Client) -> Result<(), ApiError> {
        let api_url = self.base_uri.clone().join("v1/import")?;
        let response = match &self.auth {
            ApiAuth::Basic(auth) => {
                client
                    .get(api_url)
                    .header(AUTHORIZATION, auth.clone())
                    .body(opml.to_owned())
                    .send()
                    .await?
            }
            ApiAuth::Token(auth) => {
                client
                    .get(api_url)
                    .header("X-Auth-Token", auth.clone())
                    .body(opml.to_owned())
                    .send()
                    .await?
            }
        };
        let _ = Self::parse_error(response, StatusCode::CREATED).await?;
        Ok(())
    }

    /// Create a new user on the Miniflux instance.
    /// You must be an administrator to create users.
    pub async fn create_user(
        &self,
        username: &str,
        password: &str,
        is_admin: bool,
        client: &Client,
    ) -> Result<User, ApiError> {
        let api_url = self.base_uri.clone().join("v1/users")?;
        let content = UserCreation {
            username: username.to_owned(),
            password: password.to_owned(),
            is_admin,
        };
        let response = self
            .send_request(client.post(api_url), Some(content))
            .await?;
        let response = Self::parse_error(response, StatusCode::CREATED).await?;
        let user = Self::deserialize::<User>(&response)?;
        Ok(user)
    }

    /// Update details and/or credentials of a user.
    /// You must be an administrator to update users.
    #[allow(clippy::too_many_arguments)]
    pub async fn update_user(
        &self,
        id: UserID,
        username: Option<String>,
        password: Option<String>,
        is_admin: Option<bool>,
        theme: Option<String>,
        language: Option<String>,
        timezone: Option<String>,
        entry_sorting_direction: Option<String>,
        client: &Client,
    ) -> Result<User, ApiError> {
        let api_url = self.base_uri.clone().join(&format!("v1/users/{}", id))?;
        let content = UserModification {
            username,
            password,
            is_admin,
            theme,
            language,
            timezone,
            entry_sorting_direction,
        };
        let response = self
            .send_request(client.put(api_url), Some(content))
            .await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        let user = Self::deserialize::<User>(&response)?;
        Ok(user)
    }

    /// Get the user specified when this struct was created.
    /// This API endpoint is available since Miniflux v2.0.8.
    pub async fn get_current_user(&self, client: &Client) -> Result<User, ApiError> {
        let api_url = self.base_uri.clone().join("v1/me")?;
        let response = self.send_request::<()>(client.get(api_url), None).await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        let user = Self::deserialize::<User>(&response)?;
        Ok(user)
    }

    /// Get a specific user of the Miniflux instance.
    /// You must be an administrator to fetch users.
    pub async fn get_user_by_id(&self, id: UserID, client: &Client) -> Result<User, ApiError> {
        let api_url = self.base_uri.clone().join(&format!("v1/users/{}", id))?;
        let response = self.send_request::<()>(client.post(api_url), None).await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        let user = Self::deserialize::<User>(&response)?;
        Ok(user)
    }

    /// Try to get a user by its `username`.
    /// You must be an administrator to fetch users.
    pub async fn get_user_by_name(
        &self,
        username: &str,
        client: &Client,
    ) -> Result<User, ApiError> {
        let api_url = self
            .base_uri
            .clone()
            .join(&format!("v1/users/{}", username))?;
        let response = self.send_request::<()>(client.post(api_url), None).await?;
        let response = Self::parse_error(response, StatusCode::OK).await?;
        let user = Self::deserialize::<User>(&response)?;
        Ok(user)
    }

    /// Delete a user.
    /// You must be an administrator to delete users.
    pub async fn delete_user(&self, id: UserID, client: &Client) -> Result<(), ApiError> {
        let api_url = self.base_uri.clone().join(&format!("v1/users/{}", id))?;
        let response = self
            .send_request::<()>(client.delete(api_url), None)
            .await?;
        let _ = Self::parse_error(response, StatusCode::OK).await?;
        Ok(())
    }

    /// The healthcheck endpoint is useful for monitoring and load-balancer configuration.
    pub async fn healthcheck(&self, client: &Client) -> Result<(), ApiError> {
        let api_url = self.base_uri.clone().join("healthcheck")?;
        let response = self.send_request::<()>(client.get(api_url), None).await?;
        let _ = Self::parse_error(response, StatusCode::OK).await?;
        Ok(())
    }
}
