defmodule Swoosh.Adapters.SMTP.Helpers do
  @moduledoc false

  alias Swoosh.Email

  import Swoosh.Email.Render

  @doc false
  def sender(%Email{} = email) do
    email.headers["Sender"] || elem(email.from, 1)
  end

  @doc false
  def body(email, config) do
    {message_config, config} = Keyword.split(config, [:transfer_encoding, :keep_bcc])
    {type, subtype, headers, parts} = prepare_message(email, message_config)
    {encoding_config, _config} = Keyword.split(config, [:dkim])
    mime_encode(type, subtype, headers, parts, encoding_config)
  end

  # TODO: Remove conditional handling when going 2.0
  gen_smtp_major =
    if Code.ensure_loaded?(:gen_smtp_client) do
      Application.load(:gen_smtp)

      :gen_smtp
      |> Application.spec(:vsn)
      |> to_string()
      |> Version.parse!()
      |> Map.get(:major)
    else
      0
    end

  @parameters if(gen_smtp_major >= 1, do: %{}, else: [])

  defp mime_encode(type, subtype, headers, parts, encoding_config) do
    :mimemail.encode({type, subtype, headers, @parameters, parts}, encoding_config)
  end

  @doc false
  def prepare_message(email, config) do
    email
    |> prepare_headers(config)
    |> prepare_parts(email, config)
  end

  defp prepare_headers(email, config) do
    []
    |> prepare_additional_headers(email)
    |> prepare_mime_version()
    |> prepare_reply_to(email)
    |> prepare_subject(email)
    |> prepare_cc(email)
    |> then(fn headers ->
      # bcc is deliberately omitted unless explicitly asked to keep, for service providers
      # https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
      if config[:keep_bcc] do
        prepare_bcc(headers, email)
      else
        headers
      end
    end)
    |> prepare_to(email)
    |> prepare_from(email)
  end

  defp prepare_subject(headers, %{subject: subject}) when is_binary(subject),
    do: [{"Subject", subject} | headers]

  defp prepare_from(headers, %{from: from}), do: [{"From", render_recipient(from)} | headers]

  defp prepare_to(headers, %{to: []}), do: headers
  defp prepare_to(headers, %{to: to}), do: [{"To", render_recipient(to)} | headers]

  defp prepare_cc(headers, %{cc: []}), do: headers
  defp prepare_cc(headers, %{cc: cc}), do: [{"Cc", render_recipient(cc)} | headers]

  defp prepare_bcc(headers, %{bcc: []}), do: headers
  defp prepare_bcc(headers, %{bcc: bcc}), do: [{"Bcc", render_recipient(bcc)} | headers]

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

  defp prepare_reply_to(headers, %{reply_to: reply_to}),
    do: [{"Reply-To", render_recipient(reply_to)} | headers]

  defp prepare_mime_version(headers), do: [{"MIME-Version", "1.0"} | headers]

  defp prepare_additional_headers(headers, %{headers: additional_headers}) do
    Map.to_list(additional_headers) ++ headers
  end

  defp prepare_parts(
         headers,
         %{
           attachments: [],
           html_body: html_body,
           text_body: text_body
         },
         config
       ) do
    case {text_body, html_body} do
      {text_body, nil} ->
        {"text", "plain", add_content_type_header(headers, "text/plain; charset=\"utf-8\""),
         text_body}

      {nil, html_body} ->
        {"text", "html", add_content_type_header(headers, "text/html; charset=\"utf-8\""),
         html_body}

      {text_body, html_body} ->
        parts = [
          prepare_part(:plain, text_body, config),
          prepare_part(:html, html_body, config)
        ]

        {"multipart", "alternative", headers, parts}
    end
  end

  defp prepare_parts(
         headers,
         %{
           attachments: attachments,
           html_body: html_body,
           text_body: text_body
         },
         config
       ) do
    {inline_attachments, attachments} = Enum.split_with(attachments, &(&1.type == :inline))

    content_part =
      case {prepare_part(:plain, text_body, config), prepare_part(:html, html_body, config)} do
        {text_part, nil} ->
          text_part

        {nil, html_part} ->
          html_with_line_attachments(html_part, inline_attachments)

        {text_part, html_part} ->
          html_part = html_with_line_attachments(html_part, inline_attachments)

          {"multipart", "alternative", [], %{}, [text_part, html_part]}
      end

    attachment_parts = Enum.map(attachments, &prepare_attachment(&1))

    {"multipart", "mixed", headers, [content_part | attachment_parts]}
  end

  defp prepare_part(_subtype, nil, _config), do: nil

  @content_params if(gen_smtp_major >= 1,
                    do: %{
                      content_type_params: [{"charset", "utf-8"}],
                      disposition: "inline",
                      disposition_params: []
                    },
                    else: [
                      {"content-type-params", [{"charset", "utf-8"}]},
                      {"disposition", "inline"},
                      {"disposition-params", []}
                    ]
                  )

  defp prepare_part(subtype, content, config) do
    subtype_string = to_string(subtype)
    transfer_encoding = Keyword.get(config, :transfer_encoding, "quoted-printable")

    {"text", subtype_string,
     [
       {"Content-Type", "text/#{subtype_string}; charset=\"utf-8\""},
       {"Content-Transfer-Encoding", transfer_encoding}
     ], @content_params, content}
  end

  defp html_with_line_attachments(html_part, []), do: html_part

  defp html_with_line_attachments(html_part, inline_attachments) do
    attachment_parts = Enum.map(inline_attachments, &prepare_attachment(&1))
    {"multipart", "related", [], %{}, [html_part | attachment_parts]}
  end

  defp add_content_type_header(headers, value) do
    if Enum.find(headers, fn {header_name, _} ->
         String.downcase(header_name) == "content-type"
       end) do
      headers
    else
      [{"Content-Type", value} | headers]
    end
  end

  defp prepare_attachment(
         %Swoosh.Attachment{
           cid: cid,
           filename: filename,
           content_type: content_type,
           type: attachment_type,
           headers: custom_headers
         } = attachment
       ) do
    [type, format] = String.split(content_type, "/")
    content = Swoosh.Attachment.get_content(attachment)
    transfer_encoding = transfer_encoding_for_content_type(content_type)

    case attachment_type do
      :attachment ->
        {
          type,
          format,
          [
            {"Content-Transfer-Encoding", transfer_encoding}
            | custom_headers
          ],
          attachment_content_params(:attachment, filename),
          content
        }

      :inline ->
        {
          type,
          format,
          [
            {"Content-Transfer-Encoding", transfer_encoding},
            {"Content-Id", "<#{cid || filename}>"}
            | custom_headers
          ],
          attachment_content_params(:inline, filename),
          content
        }
    end
  end

  # RFC 2046 Section 5.2: message/* types must not use base64 encoding
  # - message/rfc822 allows "7bit", "8bit", or "binary" (Section 5.2.1)
  # - message/partial must be "7bit" (Section 5.2.2)
  # - message/external-body must be "7bit" (Section 5.2.3)
  defp transfer_encoding_for_content_type("message/rfc822"), do: "8bit"
  defp transfer_encoding_for_content_type("message/" <> _), do: "7bit"
  defp transfer_encoding_for_content_type(_), do: "base64"

  if gen_smtp_major >= 1 do
    defp attachment_content_params(:attachment, filename) do
      %{
        disposition: "attachment",
        disposition_params: [{"filename", filename}]
      }
    end
  else
    defp attachment_content_params(:attachment, filename) do
      [
        {"disposition", "attachment"},
        {"disposition-params", [{"filename", filename}]}
      ]
    end
  end

  if gen_smtp_major >= 1 do
    defp attachment_content_params(:inline, filename) do
      %{
        content_type_params: [],
        disposition: "inline",
        disposition_params: [{"filename", filename}]
      }
    end
  else
    defp attachment_content_params(:inline, filename) do
      [
        {"content-type-params", []},
        {"disposition", "inline"},
        {"disposition-params", [{"filename", filename}]}
      ]
    end
  end
end
