use crate::error::Error;
use crate::mailbox::{Mailbox, MailboxData};
use crate::message::{MessageData, MessagePreviewData};
use bytes::Bytes;
use futures::{Stream, TryStreamExt};
use jiff::Timestamp;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::{ClientBuilder, header};
use serde::Deserialize;
use std::time::Duration;

/// Entry point for the temp-mail.org API.
///
/// A `Client` owns the underlying HTTP client and is used to create temporary mailboxes.
#[derive(Clone, Debug)]
pub struct Client {
    client: reqwest::Client,
}

impl Client {
    /// Creates a client with the crate's default configuration.
    pub fn new() -> Self {
        Self::default()
    }

    /// Creates a new disposable mailbox.
    pub async fn create_mailbox(&self) -> Result<Mailbox<'_>, Error> {
        let response = self
            .client
            .post("https://mob2.temp-mail.org/mailbox")
            .send()
            .await?
            .error_for_status()?;

        let data = response.json::<MailboxData>().await?;
        Ok(Mailbox::new(self, data))
    }

    pub(crate) async fn list_mailbox_messages(
        &self,
        token: &str,
        after: Option<Timestamp>,
    ) -> Result<Vec<MessagePreviewData>, Error> {
        let mut builder = self
            .client
            .get("https://mob2.temp-mail.org/messages")
            .header(header::AUTHORIZATION, self.authorization_header(token)?);
        if let Some(after) = after {
            builder = builder.query(&[("after", after.as_second())]);
        }

        let response = builder
            .send()
            .await?
            .error_for_status()?
            .json::<ListMessagesResponse>()
            .await?;
        Ok(response.messages)
    }

    pub(crate) async fn get_message(
        &self,
        token: &str,
        message_id: &str,
    ) -> Result<MessageData, Error> {
        let response = self
            .client
            .get(format!("https://mob2.temp-mail.org/messages/{message_id}"))
            .header(header::AUTHORIZATION, self.authorization_header(token)?)
            .send()
            .await?
            .error_for_status()?;
        Ok(response.json().await?)
    }

    pub(crate) async fn get_attachment(
        &self,
        token: &str,
        message_id: &str,
        attachment_id: u32,
    ) -> Result<impl Stream<Item = Result<Bytes, Error>>, Error> {
        let response = self
            .client
            .get(format!(
                "https://mob2.temp-mail.org/messages/{message_id}/attachment/{attachment_id}"
            ))
            .header(header::AUTHORIZATION, self.authorization_header(token)?)
            .send()
            .await?
            .error_for_status()?;
        let bytes_stream = response.bytes_stream();

        Ok(bytes_stream.map_err(Error::from).into_stream())
    }

    fn authorization_header(&self, token: &str) -> Result<HeaderValue, Error> {
        HeaderValue::from_str(token).map_err(|_| Error::NonAsciiMailboxToken(String::from(token)))
    }
}

/// Creates a client with the crate's default HTTP settings.
///
/// This is equivalent to calling [`Client::new`].
impl Default for Client {
    fn default() -> Self {
        let mut headers = HeaderMap::new();
        headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));

        Self {
            client: ClientBuilder::new()
                // TODO: consider retrieving the latest Android app version for User-Agent header
                .user_agent("4.09")
                .default_headers(headers)
                .referer(false)
                .read_timeout(Duration::new(5, 0))
                .build()
                .expect("failed to create reqwest client"),
        }
    }
}

/// Create [`Client`] with your own [`reqwest::Client`].
impl From<reqwest::Client> for Client {
    fn from(client: reqwest::Client) -> Self {
        Self { client }
    }
}

#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)]
struct ListMessagesResponse {
    mailbox: String,
    messages: Vec<MessagePreviewData>,
}
