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

  For reference: [MailerSend API docs](https://developers.mailersend.com/api/v1/email.html)

  **This adapter requires an API Client.** Swoosh comes with Hackney, Finch and Req out of the box.
  See the [installation section](https://hexdocs.pm/swoosh/Swoosh.html#module-installation)
  for details.

  ## Example

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

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

  ## Using with provider options

      import Swoosh.Email

      new()
      |> from({"T Stark", "tony.stark@example.com"})
      |> to({"Steve Rogers", "steve.rogers@example.com"})
      |> subject("Hello, Avengers!")
      |> html_body("<h1>Hello</h1>")
      |> text_body("Hello")
      |> put_provider_option(:tags, ["onboarding", "welcome"])
      |> put_provider_option(:track_opens, true)
      |> put_provider_option(:track_clicks, true)
      |> put_provider_option(:metadata, %{"user_id" => "123"})

  ## Using with MailerSend templates

      import Swoosh.Email

      new()
      |> from({"T Stark", "tony.stark@example.com"})
      |> to({"Steve Rogers", "steve.rogers@example.com"})
      |> put_provider_option(:template_id, "template-123")
      |> put_provider_option(:template_variables, %{
        "name" => "Steve",
        "mission" => "Project Insight"
      })

  ## Personalization without templates

  MailerSend supports `{{ variable }}` substitution in subject, HTML, and
  text body fields without requiring a template.

      import Swoosh.Email

      new()
      |> from({"T Stark", "tony.stark@example.com"})
      |> to({"Steve Rogers", "steve.rogers@example.com"})
      |> subject("Welcome {{ name }}!")
      |> html_body("<h1>Hello {{ name }}</h1>")
      |> put_provider_option(:personalization, [
        %{"email" => "steve.rogers@example.com", "data" => %{"name" => "Steve"}}
      ])

  ## Provider Options

    * `:template_id` (string) - MailerSend template ID to use instead of
      html/text body

    * `:template_variables` (map) - variables for template personalization,
      applied to all recipients via the `personalization` array

    * `:personalization` (list of maps) - per-recipient personalization data
      for non-template emails. Each map must have `"email"` and `"data"` keys.
      Values populate `{{ variable }}` placeholders in subject, HTML, and text

    * `:tags` (list of strings) - tags to categorize the email (max 5)

    * `:webhook_id` (string) - webhook ID for delivery tracking

    * `:metadata` (map) - custom metadata to attach to the email

    * `:send_at` (integer or DateTime) - unix timestamp or DateTime for
      scheduled sending

    * `:track_opens` (boolean) - enable open tracking

    * `:track_clicks` (boolean) - enable click tracking

    * `:track_content` (boolean) - enable content tracking

    * `:in_reply_to` (string) - Message-ID of the email being replied to

    * `:references` (list of strings) - list of Message-IDs that the
      current email is referencing

    * `:precedence_bulk` (boolean) - set precedence bulk header, overrides
      domain's advanced settings

    * `:list_unsubscribe` (string) - List-Unsubscribe header value per
      RFC 8058
  """

  use Swoosh.Adapter, required_config: [:api_key]

  alias Swoosh.Email

  @base_url "https://api.mailersend.com"
  @api_endpoint "/v1/email"
  @bulk_endpoint "/v1/bulk-email"

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

    case Swoosh.ApiClient.post(url, headers, body, email) do
      {:ok, 202, response_headers, response_body} ->
        id = get_header_value(response_headers, "x-message-id")

        case Swoosh.json_library().decode(response_body) do
          {:ok, %{"warnings" => warnings}} when is_list(warnings) ->
            {:ok, %{id: id, warnings: warnings}}

          _ ->
            {:ok, %{id: id}}
        end

      {: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, []}

  def deliver_many(emails, config) when is_list(emails) do
    headers = prepare_headers(config)
    body = emails |> Enum.map(&prepare_body/1) |> Swoosh.json_library().encode!()
    url = [base_url(config), @bulk_endpoint]

    case Swoosh.ApiClient.post(url, headers, body, List.first(emails)) do
      {:ok, 202, _headers, body} ->
        {:ok, [%{bulk_email_id: extract_bulk_id(body)}]}

      {: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 extract_bulk_id(body) do
    body |> Swoosh.json_library().decode!() |> Map.get("bulk_email_id")
  end

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

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

  defp prepare_body(%Email{provider_options: %{template_id: template_id}} = email) do
    %{}
    |> Map.put("template_id", template_id)
    |> prepare_from(email)
    |> prepare_to(email)
    |> prepare_cc(email)
    |> prepare_bcc(email)
    |> prepare_subject_if_present(email)
    |> prepare_reply_to(email)
    |> prepare_attachments(email)
    |> prepare_custom_headers(email)
    |> prepare_template_personalization(email)
    |> prepare_tags(email)
    |> prepare_webhook_id(email)
    |> prepare_metadata(email)
    |> prepare_send_at(email)
    |> prepare_tracking(email)
    |> prepare_in_reply_to(email)
    |> prepare_references(email)
    |> prepare_precedence_bulk(email)
    |> prepare_list_unsubscribe(email)
  end

  defp prepare_body(email) do
    %{}
    |> prepare_from(email)
    |> prepare_to(email)
    |> prepare_cc(email)
    |> prepare_bcc(email)
    |> prepare_subject(email)
    |> prepare_html(email)
    |> prepare_text(email)
    |> prepare_reply_to(email)
    |> prepare_attachments(email)
    |> prepare_custom_headers(email)
    |> prepare_personalization(email)
    |> prepare_tags(email)
    |> prepare_webhook_id(email)
    |> prepare_metadata(email)
    |> prepare_send_at(email)
    |> prepare_tracking(email)
    |> prepare_in_reply_to(email)
    |> prepare_references(email)
    |> prepare_precedence_bulk(email)
    |> prepare_list_unsubscribe(email)
  end

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

  defp prepare_to(body, %{to: []}), do: body
  defp prepare_to(body, %{to: to}), do: Map.put(body, "to", Enum.map(to, &render_recipient/1))

  defp prepare_cc(body, %{cc: []}), do: body
  defp prepare_cc(body, %{cc: cc}), do: Map.put(body, "cc", Enum.map(cc, &render_recipient/1))

  defp prepare_bcc(body, %{bcc: []}), do: body

  defp prepare_bcc(body, %{bcc: bcc}),
    do: Map.put(body, "bcc", Enum.map(bcc, &render_recipient/1))

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

  defp prepare_subject_if_present(body, %{subject: nil}), do: body
  defp prepare_subject_if_present(body, %{subject: ""}), do: body

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

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

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

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

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

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

  defp prepare_attachments(body, %{attachments: attachments}) do
    sorted =
      Enum.sort_by(attachments, fn
        %{type: :inline} -> 1
        _ -> 0
      end)

    Map.put(body, "attachments", Enum.map(sorted, &prepare_attachment/1))
  end

  defp prepare_attachment(attachment) do
    base = %{
      "content" => Swoosh.Attachment.get_content(attachment, :base64),
      "filename" => attachment.filename,
      "type" => attachment.content_type
    }

    case attachment.type do
      :inline ->
        base
        |> Map.put("disposition", "inline")
        |> put_attachment_id(attachment.cid)

      _ ->
        Map.put(base, "disposition", "attachment")
    end
  end

  defp put_attachment_id(att, cid) when is_binary(cid), do: Map.put(att, "id", cid)
  defp put_attachment_id(att, _), do: att

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

  defp prepare_custom_headers(body, %{headers: headers}) do
    formatted = Enum.map(headers, fn {name, value} -> %{"name" => name, "value" => value} end)
    Map.put(body, "headers", formatted)
  end

  # Template personalization: applies template_variables to all recipients
  defp prepare_template_personalization(body, %{
         to: to,
         cc: cc,
         bcc: bcc,
         provider_options: provider_options
       }) do
    case Map.get(provider_options, :template_variables) do
      nil ->
        body

      variables when is_map(variables) ->
        all_recipients = to ++ cc ++ bcc

        personalization =
          Enum.map(all_recipients, fn recipient ->
            %{
              "email" => recipient_email(recipient),
              "data" => variables
            }
          end)

        Map.put(body, "personalization", personalization)
    end
  end

  # Non-template personalization: user provides per-recipient data directly
  defp prepare_personalization(body, %{provider_options: %{personalization: personalization}})
       when is_list(personalization) do
    Map.put(body, "personalization", personalization)
  end

  defp prepare_personalization(body, _email), do: body

  defp prepare_tags(body, %{provider_options: %{tags: tags}}) when is_list(tags),
    do: Map.put(body, "tags", tags)

  defp prepare_tags(body, _), do: body

  defp prepare_webhook_id(body, %{provider_options: %{webhook_id: id}}),
    do: Map.put(body, "webhook_id", id)

  defp prepare_webhook_id(body, _), do: body

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

  defp prepare_metadata(body, _), do: body

  defp prepare_send_at(body, %{provider_options: %{send_at: %DateTime{} = dt}}),
    do: Map.put(body, "send_at", DateTime.to_unix(dt))

  defp prepare_send_at(body, %{provider_options: %{send_at: ts}}) when is_integer(ts),
    do: Map.put(body, "send_at", ts)

  defp prepare_send_at(body, _), do: body

  defp prepare_tracking(body, %{provider_options: provider_options}) do
    settings =
      %{}
      |> put_if_boolean("track_opens", Map.get(provider_options, :track_opens))
      |> put_if_boolean("track_clicks", Map.get(provider_options, :track_clicks))
      |> put_if_boolean("track_content", Map.get(provider_options, :track_content))

    case map_size(settings) do
      0 -> body
      _ -> Map.put(body, "settings", settings)
    end
  end

  defp prepare_tracking(body, _), do: body

  defp prepare_in_reply_to(body, %{provider_options: %{in_reply_to: value}})
       when is_binary(value),
       do: Map.put(body, "in_reply_to", value)

  defp prepare_in_reply_to(body, _), do: body

  defp prepare_references(body, %{provider_options: %{references: refs}}) when is_list(refs),
    do: Map.put(body, "references", refs)

  defp prepare_references(body, _), do: body

  defp prepare_precedence_bulk(body, %{provider_options: %{precedence_bulk: value}})
       when is_boolean(value),
       do: Map.put(body, "precedence_bulk", value)

  defp prepare_precedence_bulk(body, _), do: body

  defp prepare_list_unsubscribe(body, %{provider_options: %{list_unsubscribe: value}})
       when is_binary(value),
       do: Map.put(body, "list_unsubscribe", value)

  defp prepare_list_unsubscribe(body, _), do: body

  defp put_if_boolean(map, _key, nil), do: map
  defp put_if_boolean(map, key, value) when is_boolean(value), do: Map.put(map, key, value)
  defp put_if_boolean(map, _key, _), do: map

  defp render_recipient({name, email}) when is_binary(name) and name != "",
    do: %{"email" => email, "name" => name}

  defp render_recipient({_name, email}), do: %{"email" => email}
  defp render_recipient(email) when is_binary(email), do: %{"email" => email}

  defp recipient_email({_name, email}), do: email
  defp recipient_email(email) when is_binary(email), do: email

  defp get_header_value(headers, key) do
    key_down = String.downcase(key)

    Enum.find_value(headers, fn {k, v} ->
      if String.downcase(k) == key_down, do: v
    end)
  end
end
