use base64::Engine;
use base64::engine::general_purpose::STANDARD as base64_std;
pub use error::ApiError;
use models::Subscription;
pub use models::{
    AddCategoryRequest, Category, CategoryModificationRequest, Entries, FeedModificationRequest,
    IDRequest, MarkRequest, MultipleMarkRequest, ServerInfo, Settings, StarRequest,
    SubscribeRequest, TagRequest, User,
};
use reqwest::Client;
use reqwest::header::AUTHORIZATION;
use serde::{Deserialize, Serialize};
use url::Url;

pub mod error;
pub mod models;

#[cfg(test)]
mod tests;

pub struct CommafeedApi {
    url: Url,
    auth: String,
}

impl CommafeedApi {
    pub fn new(url: &Url, user: &str, password: &str) -> Self {
        Self {
            url: url.clone(),
            auth: Self::generate_basic_auth(user, password),
        }
    }

    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<String, ApiError> {
        let mut client = client.header(AUTHORIZATION, &self.auth);
        if let Some(json_content) = json_content {
            client = client.json(&json_content);
        }
        let response = client.send().await?;
        let response = response.error_for_status()?;
        let text = response.text().await?;

        Ok(text)
    }

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

    pub async fn get_profile(&self, client: &Client) -> Result<User, ApiError> {
        let api_url = self.url.join("user/profile")?;
        let response = self.send_request(client.get(api_url), None::<()>).await?;
        let user = Self::deserialize(&response)?;
        Ok(user)
    }

    pub async fn get_settings(&self, client: &Client) -> Result<Settings, ApiError> {
        let api_url = self.url.join("user/settings")?;
        let response = self.send_request(client.get(api_url), None::<()>).await?;
        let settings = Self::deserialize(&response)?;
        Ok(settings)
    }

    pub async fn get_server_info(&self, client: &Client) -> Result<ServerInfo, ApiError> {
        let api_url = self.url.join("server/get")?;
        let response = self.send_request(client.get(api_url), None::<()>).await?;
        let server_info = Self::deserialize(&response)?;
        Ok(server_info)
    }

    pub async fn get_category_tree(&self, client: &Client) -> Result<Category, ApiError> {
        let api_url = self.url.join("category/get")?;
        let response = self.send_request(client.get(api_url), None::<()>).await?;
        let tree = Self::deserialize(&response)?;
        Ok(tree)
    }

    pub async fn create_category(
        &self,
        name: &str,
        parent_id: Option<&str>,
        client: &Client,
    ) -> Result<i64, ApiError> {
        let api_url = self.url.join("category/add")?;
        let add_request = AddCategoryRequest {
            name: name.into(),
            parent_id: parent_id.map(String::from),
        };
        let response = self
            .send_request(client.post(api_url), Some(add_request))
            .await?;
        let category_id = Self::deserialize(&response)?;
        Ok(category_id)
    }

    pub async fn delete_category(&self, category_id: i64, client: &Client) -> Result<(), ApiError> {
        let api_url = self.url.join("category/delete")?;
        let id_request = IDRequest { id: category_id };
        self.send_request(client.post(api_url), Some(id_request))
            .await?;
        Ok(())
    }

    pub async fn modify_category(
        &self,
        category_id: i64,
        new_title: Option<&str>,
        parent_id: Option<&str>,
        position: Option<i32>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_url = self.url.join("category/modify")?;
        let modify_request = CategoryModificationRequest {
            id: category_id,
            name: new_title.map(String::from),
            parent_id: parent_id.map(String::from),
            position,
        };
        self.send_request(client.post(api_url), Some(modify_request))
            .await?;
        Ok(())
    }

    pub async fn mark_category_read(
        &self,
        category_id: &str,
        read: bool,
        older_than: Option<i64>,
        keywords: Option<&str>,
        excluded: Option<Vec<i64>>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_url = self.url.join("category/mark")?;
        let mark_request = MarkRequest {
            id: category_id.into(),
            read,
            older_than,
            keywords: keywords.map(String::from),
            excluded_subscriptions: excluded,
        };
        self.send_request(client.post(api_url), Some(mark_request))
            .await?;
        Ok(())
    }

    pub async fn set_tags(&self, tag_request: TagRequest, client: &Client) -> Result<(), ApiError> {
        let api_url = self.url.join("entry/tags")?;
        self.send_request(client.post(api_url), Some(tag_request))
            .await?;
        Ok(())
    }

    pub async fn get_tags(&self, client: &Client) -> Result<Vec<String>, ApiError> {
        let api_url = self.url.join("entry/tags")?;
        let response = self.send_request(client.get(api_url), None::<()>).await?;
        let tags = Self::deserialize(&response)?;
        Ok(tags)
    }

    pub async fn subscribe_to_feed_simple(
        &self,
        url: &str,
        client: &Client,
    ) -> Result<i64, ApiError> {
        let api_url = self.url.join("feed/subscribe")?;
        let response = self.send_request(client.get(api_url), Some(url)).await?;
        let feed_id = Self::deserialize(&response)?;
        Ok(feed_id)
    }

    pub async fn subscribe_to_feed(
        &self,
        url: &str,
        title: &str,
        parent_id: Option<&str>,
        client: &Client,
    ) -> Result<i64, ApiError> {
        let api_url = self.url.join("feed/subscribe")?;
        let subscribe_request = SubscribeRequest {
            url: url.into(),
            title: title.into(),
            category_id: parent_id.map(String::from),
        };
        let response = self
            .send_request(client.post(api_url), Some(subscribe_request))
            .await?;
        let feed_id = Self::deserialize(&response)?;
        Ok(feed_id)
    }

    pub async fn unsubscribe_from_feed(
        &self,
        feed_id: i64,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_url = self.url.join("feed/unsubscribe")?;
        let id_request = IDRequest { id: feed_id };
        self.send_request(client.post(api_url), Some(id_request))
            .await?;
        Ok(())
    }

    pub async fn fetch_feed(
        &self,
        feed_id: i64,
        client: &Client,
    ) -> Result<Subscription, ApiError> {
        let endpoint = format!("feed/get/{feed_id}");
        let api_url = self.url.join(&endpoint)?;
        let response = self.send_request(client.get(api_url), None::<()>).await?;
        let feed = Self::deserialize(&response)?;
        Ok(feed)
    }

    pub async fn mark_feed_read(
        &self,
        feed_id: &str,
        read: bool,
        older_than: Option<i64>,
        keywords: Option<&str>,
        excluded: Option<Vec<i64>>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_url = self.url.join("feed/mark")?;
        let mark_request = MarkRequest {
            id: feed_id.into(),
            read,
            older_than,
            keywords: keywords.map(String::from),
            excluded_subscriptions: excluded,
        };
        self.send_request(client.post(api_url), Some(mark_request))
            .await?;
        Ok(())
    }

    pub async fn modify_feed(
        &self,
        feed_id: i64,
        new_title: Option<&str>,
        category_id: Option<&str>,
        position: Option<i32>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_url = self.url.join("feed/modify")?;
        let modify_request = FeedModificationRequest {
            id: feed_id,
            name: new_title.map(String::from),
            category_id: category_id.map(String::from),
            position,
            filter: None,
        };
        self.send_request(client.post(api_url), Some(modify_request))
            .await?;
        Ok(())
    }

    pub async fn import_opml(&self, opml: &str, client: &Client) -> Result<(), ApiError> {
        let api_url = self.url.join("feed/import")?;
        self.send_request(client.post(api_url), Some(opml)).await?;
        Ok(())
    }

    pub async fn export_opml(&self, client: &Client) -> Result<String, ApiError> {
        let api_url = self.url.join("feed/export")?;
        let opml = self.send_request(client.get(api_url), None::<()>).await?;
        Ok(opml)
    }

    pub async fn get_favicon(&self, feed_id: i64, client: &Client) -> Result<Vec<u8>, ApiError> {
        let api_url = self.url.join(&format!("feed/favicon/{feed_id}"))?;

        let request = client.get(api_url).header(AUTHORIZATION, &self.auth);
        let response = request.send().await?;
        let response = response.error_for_status()?;
        let data = response.bytes().await?;
        Ok(data.to_vec())
    }

    pub async fn mark_entry_read(
        &self,
        id: &str,
        read: bool,
        older_than: Option<i64>,
        keywords: Option<&str>,
        excluded: Option<Vec<i64>>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_url = self.url.join("entry/mark")?;
        let mark_request = MarkRequest {
            id: id.into(),
            read,
            older_than,
            keywords: keywords.map(String::from),
            excluded_subscriptions: excluded,
        };
        self.send_request(client.post(api_url), Some(mark_request))
            .await?;
        Ok(())
    }

    pub async fn mark_entry_starred(
        &self,
        request: StarRequest,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_url = self.url.join("entry/star")?;
        self.send_request(client.post(api_url), Some(request))
            .await?;
        Ok(())
    }

    pub async fn mark_multiple_entries_read(
        &self,
        requests: Vec<MarkRequest>,
        client: &Client,
    ) -> Result<(), ApiError> {
        let api_url = self.url.join("entry/markMultiple")?;
        let mark_request = MultipleMarkRequest { requests };
        self.send_request(client.post(api_url), Some(mark_request))
            .await?;
        Ok(())
    }

    #[allow(clippy::too_many_arguments)]
    pub async fn get_category_entries(
        &self,
        category_id: &str,
        read: bool,
        newer_than: Option<i64>,
        offset: Option<i32>,
        limit: Option<i32>,
        order: Option<&str>,
        keywords: Option<&str>,
        only_ids: Option<bool>,
        excluded_subscriptions: Option<&str>,
        tag: Option<&str>,
        client: &Client,
    ) -> Result<Entries, ApiError> {
        let api_url = self.url.join("category/entries")?;
        let read_type = if read { "all" } else { "unread" };
        let mut request = client
            .get(api_url)
            .query(&[("id", category_id), ("readType", read_type)]);

        if let Some(newer_than) = newer_than {
            request = request.query(&[("newerThan", newer_than)]);
        }

        if let Some(offset) = offset {
            request = request.query(&[("offset", offset)]);
        }

        if let Some(limit) = limit {
            request = request.query(&[("limit", limit)]);
        }

        if let Some(order) = order {
            request = request.query(&[("order", order)]);
        }

        if let Some(keywords) = keywords {
            request = request.query(&[("keywords", keywords)]);
        }

        if let Some(only_ids) = only_ids {
            request = request.query(&[("onlyIds", only_ids)]);
        }

        if let Some(excluded_subscriptions) = excluded_subscriptions {
            request = request.query(&[("excludedSubscriptionIds", excluded_subscriptions)]);
        }

        if let Some(tag) = tag {
            request = request.query(&[("tag", tag)]);
        }

        let response = self.send_request(request, None::<()>).await?;
        let entries = Self::deserialize(&response)?;
        Ok(entries)
    }

    #[allow(clippy::too_many_arguments)]
    pub async fn get_feed_entries(
        &self,
        feed_id: &str,
        read: bool,
        newer_than: Option<i64>,
        offset: Option<i32>,
        limit: Option<i32>,
        order: Option<&str>,
        keywords: Option<&str>,
        only_ids: Option<bool>,
        client: &Client,
    ) -> Result<Entries, ApiError> {
        let api_url = self.url.join("feed/entries")?;
        let read_type = if read { "all" } else { "unread" };
        let mut request = client
            .get(api_url)
            .query(&[("id", feed_id), ("readType", read_type)]);

        if let Some(newer_than) = newer_than {
            request = request.query(&[("newerThan", newer_than)]);
        }

        if let Some(offset) = offset {
            request = request.query(&[("offset", offset)]);
        }

        if let Some(limit) = limit {
            request = request.query(&[("limit", limit)]);
        }

        if let Some(order) = order {
            request = request.query(&[("order", order)]);
        }

        if let Some(keywords) = keywords {
            request = request.query(&[("keywords", keywords)]);
        }

        if let Some(only_ids) = only_ids {
            request = request.query(&[("onlyIds", only_ids)]);
        }

        let response = self.send_request(request, None::<()>).await?;
        let entries = Self::deserialize(&response)?;
        Ok(entries)
    }
}
