defmodule Swoosh.Adapters.Postmark do
  @moduledoc ~S"""
  An adapter that sends email using the Postmark API.

  For reference: [Postmark API docs](http://developer.postmarkapp.com/developer-send-api.html)

  ## Example

      # config/config.exs
      config :sample, Sample.Mailer,
        adapter: Swoosh.Adapters.Postmark,
        api_key: "my-api-key"

      # lib/sample/mailer.ex
      defmodule Sample.Mailer do
        use Swoosh.Mailer, otp_app: :sample
      end

  ## Example of sending emails using templates

  This will use Postmark's `withTemplate` endpoint.

      import Swoosh.Email

      new()
      |> from({"T Stark", "tony.stark@example.com"})
      |> to({"Steve Rogers", "steve.rogers@example.com"})
      |> put_provider_option(:template_id, "123456")
      |> put_provider_option(:template_model, %{name: "Steve", email: "steve@avengers.com"})

  You can also use `template_alias` instead of `template_id`, if you use Postmark's
  [TemplateAlias](https://postmarkapp.com/developer/api/templates-api#email-with-template) feature.

  Note that you must include the `:template_model` provider option even if your template
  has no variables to interpolate. In this case you can pass an empty map:

      put_provider_option(email, :template_model, %{})

  When sending batch emails using `:deliver_many` do not mix emails using
  templates with non-template emails. The use of templates impacts the API
  endpoint used and so the batch email collection should be of the same format.

  ## Example of sending emails with a tag

  This will add a tag to the sent Postmark's email.

      import Swoosh.Email

      new()
      |> from({"T Stark", "tony.stark@example.com"})
      |> to({"Steve Rogers", "steve.rogers@example.com"})
      |> subject("Hello, Avengers!")
      |> put_provider_option(:tag, "some tag")

  ## Provider Options

    * `:message_stream` (string) – `MessageStream`, configure the message stream
      for the email

    * `:metadata` (map) - `Metadata`, add metadata to an email

    * `:tag` (string) - `Tag`, to categorize outgoing email

    * `:template_id` (string) - `TemplateId`, the template used when sending
      email and only required if `:template_alias` is not specified

    * `:template_alias` (string), `TemplateAlias`, the alias of a template used
      when sending email and only required if `:template_id` is not specified

    * `:template_model` (map), `TemplateModel`, a map of key/value field to be
      used in the `HtmlBody`, `TextBody`, and `Subject` field in the template,
      required alongside `:template_id`/`:template_alias`

    * `:track_opens` (boolean) - `TrackOpens`, specify if open tracking needs to be enabled for this email.

    * `:track_links` (string) - `TrackOpens`, specify if link tracking needs to be enabled for this email.
       Valid values are: `None`, `HtmlAndText`, `HtmlOnly`, `TextOnly`

    * `:inline_css` (boolean) - `InlineCss`, specify if Postmark should apply the style blocks as inline
      attributes to the rendered HTML content. Default is true.
  """

  use Swoosh.Adapter, required_config: [:api_key]

  alias Swoosh.Email
  import Swoosh.Email.Render

  @base_url "https://api.postmarkapp.com"

  def deliver(%Email{} = email, config \\ []) do
    headers = prepare_headers(config)
    params = email |> prepare_body() |> Swoosh.json_library().encode!
    url = [base_url(config), api_endpoint(email)]

    case Swoosh.ApiClient.post(url, headers, params, email) do
      {:ok, 200, _headers, body} ->
        {:ok, %{id: Swoosh.json_library().decode!(body)["MessageID"]}}

      {:ok, code, _headers, body} when code > 399 ->
        case Swoosh.json_library().decode(body) do
          {:ok, error} -> {:error, {code, error}}
          {:error, _} -> {:error, {code, body}}
        end

      {:error, reason} ->
        {:error, reason}
    end
  end

  def deliver_many(emails, config \\ [])

  def deliver_many([], _config) do
    {:ok, []}
  end

  def deliver_many(emails, config) when is_list(emails) do
    headers = prepare_headers(config)
    body = emails |> prepare_body() |> Swoosh.json_library().encode!
    url = [base_url(config), api_endpoint(List.first(emails), true)]

    case Swoosh.ApiClient.post(url, headers, body, List.first(emails)) do
      {:ok, 200, _headers, body} ->
        results =
          Enum.map(Swoosh.json_library().decode!(body), fn email_result ->
            %{
              id: email_result["MessageID"],
              error_code: email_result["ErrorCode"],
              message: email_result["Message"],
              to: email_result["To"],
              submitted_at: email_result["SubmittedAt"]
            }
          end)

        {:ok, results}

      {:ok, code, _headers, body} when code > 399 ->
        case Swoosh.json_library().decode(body) do
          {:ok, error} -> {:error, {code, error}}
          {:error, _} -> {:error, {code, body}}
        end

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp base_url(config), do: config[:base_url] || @base_url

  defp prepare_headers(config) do
    [
      {"User-Agent", "swoosh/#{Swoosh.version()}"},
      {"X-Postmark-Server-Token", config[:api_key]},
      {"Content-Type", "application/json"},
      {"Accept", "application/json"}
    ]
  end

  defp api_endpoint(email, batch \\ false) do
    case {email_uses_template?(email), batch} do
      {true, true} -> "/email/batchWithTemplates"
      {true, false} -> "/email/withTemplate"
      {false, true} -> "/email/batch"
      {false, false} -> "/email"
    end
  end

  defp email_uses_template?(%Email{} = email) do
    case email do
      %{provider_options: %{template_id: _, template_model: _}} -> true
      %{provider_options: %{template_alias: _, template_model: _}} -> true
      _ -> false
    end
  end

  defp prepare_body(emails) when is_list(emails) do
    if email_uses_template?(List.first(emails)) do
      %{"Messages" => Enum.map(emails, &prepare_body/1)}
    else
      Enum.map(emails, &prepare_body/1)
    end
  end

  defp prepare_body(email) do
    %{}
    |> prepare_from(email)
    |> prepare_to(email)
    |> prepare_subject(email)
    |> prepare_html(email)
    |> prepare_text(email)
    |> prepare_cc(email)
    |> prepare_bcc(email)
    |> prepare_attachments(email)
    |> prepare_reply_to(email)
    |> prepare_template(email)
    |> prepare_custom_headers(email)
    |> prepare_tag(email)
    |> prepare_metadata(email)
    |> prepare_message_stream(email)
    |> prepare_track_opens(email)
    |> prepare_track_links(email)
    |> prepare_inline_css(email)
  end

  defp prepare_from(body, %{from: from}), do: Map.put(body, "From", render_recipient(from))

  defp prepare_to(body, %{to: to}), do: Map.put(body, "To", render_recipient(to))

  defp prepare_cc(body, %{cc: []}), do: body
  defp prepare_cc(body, %{cc: cc}), do: Map.put(body, "Cc", render_recipient(cc))

  defp prepare_bcc(body, %{bcc: []}), do: body
  defp prepare_bcc(body, %{bcc: bcc}), do: Map.put(body, "Bcc", render_recipient(bcc))

  defp prepare_attachments(body, %{attachments: []}), do: body

  defp prepare_attachments(body, %{attachments: attachments}) do
    Map.put(body, "Attachments", Enum.map(attachments, &prepare_attachment/1))
  end

  defp prepare_attachment(attachment) do
    attachment_data = %{
      "Name" => attachment.filename,
      "ContentType" => attachment.content_type,
      "Content" => Swoosh.Attachment.get_content(attachment, :base64)
    }

    case attachment.type do
      :attachment ->
        attachment_data

      :inline ->
        Map.put(attachment_data, "ContentID", "cid:#{attachment.cid}")
    end
  end

  defp prepare_reply_to(body, %{reply_to: nil}), do: body

  defp prepare_reply_to(body, %{reply_to: {_name, address}}),
    do: Map.put(body, "ReplyTo", address)

  defp prepare_subject(body, %{subject: ""}), do: body
  defp prepare_subject(body, %{subject: subject}), do: Map.put(body, "Subject", subject)

  defp prepare_text(body, %{text_body: nil}), do: body
  defp prepare_text(body, %{text_body: text_body}), do: Map.put(body, "TextBody", text_body)

  defp prepare_html(body, %{html_body: nil}), do: body
  defp prepare_html(body, %{html_body: html_body}), do: Map.put(body, "HtmlBody", html_body)

  # example custom vars
  #
  # %{
  #   "template_id"    => 123,
  #   "template_model" => %{"name": 1, "company": 2}
  # }
  #
  # Or, using template_alias
  #
  # %{
  #   "template_alias" => "welcome",
  #   "template_model" => %{"name": 1, "company": 2}
  # }
  defp prepare_template(body, %{provider_options: provider_options}),
    do: Enum.reduce(provider_options, body, &put_in_body/2)

  defp prepare_template(body, _email), do: body

  defp put_in_body({:template_model, val}, body_acc), do: Map.put(body_acc, "TemplateModel", val)
  defp put_in_body({:template_id, val}, body_acc), do: Map.put(body_acc, "TemplateId", val)
  defp put_in_body({:template_alias, val}, body_acc), do: Map.put(body_acc, "TemplateAlias", val)
  defp put_in_body(_, body_acc), do: body_acc

  defp prepare_custom_headers(body, %{headers: headers}) when map_size(headers) == 0, do: body

  defp prepare_custom_headers(body, %{headers: headers}) do
    custom_headers = Enum.map(headers, fn {k, v} -> %{"Name" => k, "Value" => v} end)
    Map.put(body, "Headers", custom_headers)
  end

  defp prepare_tag(body, %{provider_options: %{tag: tag_value}}) do
    Map.put(body, "Tag", tag_value)
  end

  defp prepare_tag(body, _), do: body

  defp prepare_metadata(body, %{provider_options: %{metadata: metadata}}) do
    Map.put(body, "Metadata", metadata)
  end

  defp prepare_metadata(body, _), do: body

  defp prepare_message_stream(body, %{provider_options: %{message_stream: value}}),
    do: Map.put(body, "MessageStream", value)

  defp prepare_message_stream(body, _), do: body

  defp prepare_track_opens(body, %{provider_options: %{track_opens: value}}),
    do: Map.put(body, "TrackOpens", value)

  defp prepare_track_opens(body, _), do: body

  defp prepare_track_links(body, %{provider_options: %{track_links: value}}),
    do: Map.put(body, "TrackLinks", value)

  defp prepare_track_links(body, _), do: body

  defp prepare_inline_css(body, %{provider_options: %{inline_css: value}}),
    do: Map.put(body, "InlineCss", value)

  defp prepare_inline_css(body, _), do: body
end
