///|
pub struct Client {
  api_key : String
  base_url : String
}

///|
pub fn Client::new(
  api_key : String,
  base_url? : String,
) -> Result[Client, ResendError] {
  if api_key.trim().is_empty() {
    return Err(InvalidInput("API key must not be empty"))
  }
  let root = base_url.unwrap_or("https://api.resend.com")
  if root.trim().is_empty() {
    return Err(InvalidInput("base URL must not be empty"))
  }
  let normalized = match root.strip_suffix("/") {
    Some(value) => value.to_owned()
    None => root
  }
  Ok({ api_key, base_url: normalized, })
}

///|
fn Client::headers(
  self : Client,
  idempotency_key? : String,
) -> Result[Array[(String, String)], ResendError] {
  let headers = [
    ("Authorization", "Bearer " + self.api_key),
    ("Content-Type", "application/json"),
    ("User-Agent", "moon-resend/0.1.0"),
  ]
  match idempotency_key {
    Some(value) => {
      if value.trim().is_empty() {
        return Err(InvalidInput("idempotency key must not be empty"))
      }
      if value.length() > 256 {
        return Err(
          InvalidInput("idempotency key must not exceed 256 characters"),
        )
      }
      headers.push(("Idempotency-Key", value))
    }
    None => ()
  }
  Ok(headers)
}

///|
pub fn Client::send_request(
  self : Client,
  params : SendEmailParams,
  idempotency_key? : String,
) -> Result[HttpRequest, ResendError] {
  match validate_email(params) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  Ok({
    verb: "POST",
    url: self.base_url + "/emails",
    headers: match self.headers(idempotency_key?) {
      Ok(value) => value
      Err(error) => return Err(error)
    },
    body: Some(params.json_string()),
  })
}

///|
fn validate_email(params : SendEmailParams) -> Result[Unit, ResendError] {
  if params.from.trim().is_empty() {
    return Err(InvalidInput("sender must not be empty"))
  }
  if params.to.length() == 0 {
    return Err(InvalidInput("at least one recipient is required"))
  }
  for recipient in params.to {
    if recipient.trim().is_empty() {
      return Err(InvalidInput("recipients must not be empty"))
    }
  }
  if params.subject.trim().is_empty() {
    return Err(InvalidInput("subject must not be empty"))
  }
  if params.html is None && params.text is None {
    return Err(InvalidInput("either html or text content is required"))
  }
  let has_html = match params.html {
    Some(value) => !value.trim().is_empty()
    None => false
  }
  let has_text = match params.text {
    Some(value) => !value.trim().is_empty()
    None => false
  }
  if !has_html && !has_text {
    return Err(InvalidInput("email content must not be empty"))
  }
  for attachment in params.attachments {
    if attachment.filename.trim().is_empty() {
      return Err(InvalidInput("attachment filename must not be empty"))
    }
    if attachment.content.is_empty() {
      return Err(InvalidInput("attachment content must not be empty"))
    }
  }
  Ok(())
}

///|
/// Build a POST /emails/batch request. Resend currently accepts up to 100 items.
pub fn Client::batch_send_request(
  self : Client,
  emails : Array[SendEmailParams],
  idempotency_key? : String,
) -> Result[HttpRequest, ResendError] {
  if emails.length() == 0 {
    return Err(InvalidInput("batch must contain at least one email"))
  }
  if emails.length() > 100 {
    return Err(InvalidInput("batch must not contain more than 100 emails"))
  }
  for email in emails {
    match validate_email(email) {
      Err(error) => return Err(error)
      Ok(_) => ()
    }
  }
  let body = Json::array(emails.map(email => email.to_json())).stringify()
  Ok({
    verb: "POST",
    url: self.base_url + "/emails/batch",
    headers: match self.headers(idempotency_key?) {
      Ok(value) => value
      Err(error) => return Err(error)
    },
    body: Some(body),
  })
}

///|
pub fn Client::retrieve_request(
  self : Client,
  id : String,
) -> Result[HttpRequest, ResendError] {
  if id.trim().is_empty() {
    return Err(InvalidInput("email id must not be empty"))
  }
  Ok({
    verb: "GET",
    url: self.base_url + "/emails/" + id,
    headers: self.headers().unwrap(),
    body: None,
  })
}

///|
/// Reschedule an email that has not been sent yet.
pub fn Client::update_request(
  self : Client,
  id : String,
  params : UpdateEmailParams,
) -> Result[HttpRequest, ResendError] {
  if id.trim().is_empty() {
    return Err(InvalidInput("email id must not be empty"))
  }
  if params.scheduled_at.trim().is_empty() {
    return Err(InvalidInput("scheduled_at must not be empty"))
  }
  Ok({
    verb: "PATCH",
    url: self.base_url + "/emails/" + id,
    headers: self.headers().unwrap(),
    body: Some(params.json_string()),
  })
}

///|
pub fn Client::cancel_request(
  self : Client,
  id : String,
) -> Result[HttpRequest, ResendError] {
  if id.trim().is_empty() {
    return Err(InvalidInput("email id must not be empty"))
  }
  Ok({
    verb: "POST",
    url: self.base_url + "/emails/" + id + "/cancel",
    headers: self.headers().unwrap(),
    body: None,
  })
}

///|
/// Parse the shared response used by send and cancel.
pub fn Client::parse_id_response(
  _self : Client,
  response : HttpResponse,
) -> Result[EmailId, ResendError] {
  if response.status >= 200 && response.status < 300 {
    parse_email_id(response.body)
  } else {
    Err(Api(parse_api_error(response.status, response.body)))
  }
}

///|
pub fn Client::parse_batch_response(
  _self : Client,
  response : HttpResponse,
) -> Result[BatchEmailIds, ResendError] {
  if response.status >= 200 && response.status < 300 {
    parse_batch_email_ids(response.body)
  } else {
    Err(Api(parse_api_error(response.status, response.body)))
  }
}

///|
pub fn Client::parse_retrieve_response(
  _self : Client,
  response : HttpResponse,
) -> Result[RetrievedEmail, ResendError] {
  if response.status >= 200 && response.status < 300 {
    parse_retrieved_email(response.body)
  } else {
    Err(Api(parse_api_error(response.status, response.body)))
  }
}