mod error;
pub mod models;

#[cfg(test)]
mod tests;

pub use crate::error::ApiError;

use base64::engine::general_purpose::STANDARD as base64_std;
use base64::Engine;
use models::{
    Feed, FeedCreateInput, FeedMoveInput, FeedRenameInput, FeedsResponse, Folder, FolderName,
    FoldersResponse, Item, ItemMarkAllInput, ItemMarkInput, ItemQueryInput, ItemType,
    ItemUpdatedQueryInput, ItemsResponse, Version,
};
use reqwest::{header::AUTHORIZATION, Client, Response, StatusCode};
use serde::Deserialize;
use url::Url;

type FolderID = i64;
type FeedID = i64;
type ItemID = i64;

pub struct NextcloudNewsApi {
    base_uri: Url,
    auth: String,
}

impl NextcloudNewsApi {
    /// Create a new instance of the NextcloudNewsApi.
    /// - `url`: url of the hosted Nextcloud instance (e.g. `https://cloud.test.com/`)
    /// - `username`: user existing on the Nextcloud instance
    /// - `password`: password of the user
    pub fn new(url: &Url, username: String, password: String) -> Result<Self, ApiError> {
        Ok(Self {
            base_uri: Self::sanitize_base_url(url)?,
            auth: Self::generate_basic_auth(&username, &password),
        })
    }

    fn sanitize_base_url(url: &Url) -> Result<Url, ApiError> {
        let url_str = url.as_str();

        let new_url = if !url_str.ends_with('/') {
            Url::parse(&format!("{url_str}/"))?
        } else {
            url.clone()
        };

        Ok(new_url)
    }

    fn parse_error(response: &Response, expected_status: StatusCode) -> Result<(), ApiError> {
        let status = response.status();
        if status != expected_status {
            log::error!("unexpected http status {}", status);
            Err(ApiError::from(status))
        } else {
            Ok(())
        }
    }

    async fn deserialize<T: for<'a> Deserialize<'a>>(
        response: Response,
        expected_status: Option<StatusCode>,
    ) -> Result<T, ApiError> {
        if let Some(expected_status) = expected_status {
            Self::parse_error(&response, expected_status)?;
        }

        let json = response.text().await?;
        let result: T =
            serde_json::from_str(&json).map_err(|source| ApiError::Json { source, json })?;
        Ok(result)
    }

    fn get_api_1_2_uri(&self) -> Result<Url, ApiError> {
        Ok(self.base_uri.join("index.php/apps/news/api/v1-2/")?)
    }

    fn get_api_1_3_uri(&self) -> Result<Url, ApiError> {
        Ok(self.base_uri.join("index.php/apps/news/api/v1-3/")?)
    }

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

    pub async fn get_version(&self, client: &Client) -> Result<Version, ApiError> {
        let version = self.get_version_1_3(client).await;

        if let Ok(version) = version {
            Ok(version)
        } else {
            self.get_version_1_2(client).await
        }
    }

    async fn get_version_1_3(&self, client: &Client) -> Result<Version, ApiError> {
        let api_url = self.get_api_1_3_uri()?.join("version")?;

        let response = client
            .get(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .send()
            .await?;

        Self::deserialize(response, Some(StatusCode::OK)).await
    }

    async fn get_version_1_2(&self, client: &Client) -> Result<Version, ApiError> {
        let api_url = self.get_api_1_2_uri()?.join("version")?;

        let response = client
            .get(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .send()
            .await?;

        Self::deserialize(response, Some(StatusCode::OK)).await
    }

    pub async fn get_folders(&self, client: &Client) -> Result<Vec<Folder>, ApiError> {
        let api_url = self.get_api_1_3_uri()?.join("folders")?;

        let response = client
            .get(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .send()
            .await?;

        let FoldersResponse { folders } = Self::deserialize(response, Some(StatusCode::OK)).await?;
        Ok(folders)
    }

    pub async fn create_folder(&self, client: &Client, name: &str) -> Result<Folder, ApiError> {
        let api_url = self.get_api_1_3_uri()?.join("folders")?;

        let input = FolderName { name: name.into() };

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        let FoldersResponse { folders } = Self::deserialize(response, Some(StatusCode::OK)).await?;

        let folder = match folders.into_iter().nth(0) {
            Some(folder) => folder,
            None => return Err(ApiError::Input),
        };

        Ok(folder)
    }

    pub async fn delete_folder(
        &self,
        client: &Client,
        folder_id: FolderID,
    ) -> Result<(), ApiError> {
        let api_url = self
            .get_api_1_3_uri()?
            .join(&format!("folders/{}", folder_id))?;

        let response = client
            .delete(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn rename_folder(
        &self,
        client: &Client,
        folder_id: FolderID,
        new_name: &str,
    ) -> Result<(), ApiError> {
        let api_url = self
            .get_api_1_3_uri()?
            .join(&format!("folders/{}", folder_id))?;

        let input = FolderName {
            name: new_name.into(),
        };

        let response = client
            .put(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn mark_folder(
        &self,
        client: &Client,
        folder_id: FolderID,
        newest_item_id: ItemID,
    ) -> Result<(), ApiError> {
        let api_url = self
            .get_api_1_3_uri()?
            .join(&format!("folders/{}/read", folder_id))?;

        let input = ItemMarkAllInput { newest_item_id };

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn get_feeds(&self, client: &Client) -> Result<Vec<Feed>, ApiError> {
        let api_url = self.get_api_1_3_uri()?.join("feeds")?;

        let response = client
            .get(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .send()
            .await?;

        let FeedsResponse { feeds, .. } = Self::deserialize(response, Some(StatusCode::OK)).await?;
        Ok(feeds)
    }

    pub async fn create_feed(
        &self,
        client: &Client,
        url: &str,
        folder_id: Option<FolderID>,
    ) -> Result<Feed, ApiError> {
        let api_url = self.get_api_1_3_uri()?.join("feeds")?;

        let input = FeedCreateInput {
            url: url.into(),
            folder_id,
        };

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        let FeedsResponse { feeds, .. } = Self::deserialize(response, Some(StatusCode::OK)).await?;

        let feed = match feeds.into_iter().nth(0) {
            Some(feed) => feed,
            None => return Err(ApiError::Input),
        };

        Ok(feed)
    }

    pub async fn delete_feed(&self, client: &Client, feed_id: FeedID) -> Result<(), ApiError> {
        let api_url = self
            .get_api_1_3_uri()?
            .join(&format!("feeds/{}", feed_id))?;

        let response = client
            .delete(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn move_feed(
        &self,
        client: &Client,
        feed_id: FeedID,
        folder_id: Option<FolderID>,
    ) -> Result<(), ApiError> {
        let api_url = self
            .get_api_1_3_uri()?
            .join(&format!("feeds/{}/move", feed_id))?;

        let input = FeedMoveInput { folder_id };

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn rename_feed(
        &self,
        client: &Client,
        feed_id: FeedID,
        new_name: &str,
    ) -> Result<(), ApiError> {
        let api_url = self
            .get_api_1_3_uri()?
            .join(&format!("feeds/{}/rename", feed_id))?;

        let input = FeedRenameInput {
            feed_title: new_name.into(),
        };

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn mark_feed(
        &self,
        client: &Client,
        feed_id: FeedID,
        newest_item_id: ItemID,
    ) -> Result<(), ApiError> {
        let api_url = self
            .get_api_1_3_uri()?
            .join(&format!("feeds/{}/read", feed_id))?;

        let input = ItemMarkAllInput { newest_item_id };

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    #[allow(clippy::too_many_arguments)]
    pub async fn get_items(
        &self,
        client: &Client,
        batch_size: i64,
        offset: Option<u64>,
        item_type: Option<ItemType>,
        id: Option<u64>,
        get_read: Option<bool>,
        oldest_first: Option<bool>,
    ) -> Result<Vec<Item>, ApiError> {
        let api_url = self.get_api_1_3_uri()?.join("items")?;

        let input = ItemQueryInput {
            batch_size,
            offset,
            item_type,
            id,
            get_read,
            oldest_first,
        };

        let response = client
            .get(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        let ItemsResponse { items, .. } = Self::deserialize(response, Some(StatusCode::OK)).await?;
        Ok(items)
    }

    pub async fn get_updated_items(
        &self,
        client: &Client,
        last_modified: u64,
        item_type: Option<ItemType>,
        id: Option<u64>,
    ) -> Result<Vec<Item>, ApiError> {
        let api_url = self.get_api_1_3_uri()?.join("items/updated")?;

        let input = ItemUpdatedQueryInput {
            last_modified,
            item_type,
            id,
        };

        let response = client
            .get(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        let ItemsResponse { items, .. } = Self::deserialize(response, Some(StatusCode::OK)).await?;
        Ok(items)
    }

    pub async fn mark_item_read(&self, client: &Client, item_id: ItemID) -> Result<(), ApiError> {
        let api_url = self
            .get_api_1_3_uri()?
            .join(&format!("items/{}/read", item_id))?;

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn mark_items_read(
        &self,
        client: &Client,
        item_ids: Vec<ItemID>,
    ) -> Result<(), ApiError> {
        let api_url = self.get_api_1_3_uri()?.join("items/read/multiple")?;

        let input = ItemMarkInput { item_ids };

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn mark_item_unread(&self, client: &Client, item_id: ItemID) -> Result<(), ApiError> {
        let api_url = self
            .get_api_1_3_uri()?
            .join(&format!("items/{}/unread", item_id))?;

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn mark_items_unread(
        &self,
        client: &Client,
        item_ids: Vec<ItemID>,
    ) -> Result<(), ApiError> {
        let api_url = self.get_api_1_3_uri()?.join("items/unread/multiple")?;

        let input = ItemMarkInput { item_ids };

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn mark_item_starred(
        &self,
        client: &Client,
        item_id: ItemID,
    ) -> Result<(), ApiError> {
        let api_url = self
            .get_api_1_3_uri()?
            .join(&format!("items/{}/star", item_id))?;

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn mark_items_starred(
        &self,
        client: &Client,
        item_ids: Vec<ItemID>,
    ) -> Result<(), ApiError> {
        let api_url = self.get_api_1_3_uri()?.join("items/star/multiple")?;

        let input = ItemMarkInput { item_ids };

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn mark_item_unstarred(
        &self,
        client: &Client,
        item_id: ItemID,
    ) -> Result<(), ApiError> {
        let api_url = self
            .get_api_1_3_uri()?
            .join(&format!("items/{}/unstar", item_id))?;

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn mark_items_unstarred(
        &self,
        client: &Client,
        item_ids: Vec<ItemID>,
    ) -> Result<(), ApiError> {
        let api_url = self.get_api_1_3_uri()?.join("items/unstar/multiple")?;

        let input = ItemMarkInput { item_ids };

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }

    pub async fn mark_all_items_read(
        &self,
        client: &Client,
        newest_item_id: ItemID,
    ) -> Result<(), ApiError> {
        let api_url = self.get_api_1_3_uri()?.join("items/read")?;

        let input = ItemMarkAllInput { newest_item_id };

        let response = client
            .post(api_url)
            .header(AUTHORIZATION, self.auth.clone())
            .json(&input)
            .send()
            .await?;

        Self::parse_error(&response, StatusCode::OK)?;
        Ok(())
    }
}
