defmodule WhoNeedHelp.Email.UnisenderGoAdapter do
  @moduledoc """
  Swoosh adapter for the UniSender Go transactional Web API.

  The adapter deliberately accepts the regular `Swoosh.Email` structure used
  by the rest of the application. Delivery therefore remains provider-neutral
  and can be switched back to SMTP through runtime configuration.
  """

  @behaviour Swoosh.Adapter

  alias Swoosh.Email

  @default_base_url "https://goapi.unisender.ru/ru/transactional/api/v1"
  @send_path "/email/send.json"
  @max_recipients 500
  @max_headers 50
  @max_idempotence_key_length 64

  @impl true
  def validate_config(config) do
    validate_api_key!(Keyword.get(config, :api_key))
    validate_base_url!(Keyword.get(config, :base_url, @default_base_url))
    validate_skip_unsubscribe!(Keyword.get(config, :skip_unsubscribe, false))
    :ok
  end

  @impl true
  def validate_dependency do
    Swoosh.Adapter.validate_dependency([Req, Jason])
  end

  @impl true
  def deliver(%Email{} = email, config) do
    with :ok <- validate_supported_email(email),
         {:ok, body} <- encode_payload(email, config),
         {:ok, status, _headers, response_body} <-
           Swoosh.ApiClient.post(
             endpoint(config),
             request_headers(Keyword.fetch!(config, :api_key)),
             body,
             with_client_options(email, config)
           ) do
      decode_response(status, response_body)
    end
  end

  defp validate_supported_email(%Email{to: []}), do: {:error, :to_not_set}

  defp validate_supported_email(%Email{from: nil}), do: {:error, :from_not_set}

  defp validate_supported_email(%Email{subject: subject}) when subject in [nil, ""],
    do: {:error, :subject_not_set}

  defp validate_supported_email(%Email{to: recipients})
       when length(recipients) > @max_recipients,
       do: {:error, {:too_many_recipients, @max_recipients}}

  defp validate_supported_email(%Email{cc: [_ | _]}),
    do: {:error, {:unsupported_email_feature, :cc}}

  defp validate_supported_email(%Email{bcc: [_ | _]}),
    do: {:error, {:unsupported_email_feature, :bcc}}

  defp validate_supported_email(%Email{attachments: [_ | _]}),
    do: {:error, {:unsupported_email_feature, :attachments}}

  defp validate_supported_email(%Email{reply_to: reply_to}) when is_list(reply_to),
    do: {:error, {:unsupported_email_feature, :multiple_reply_to}}

  defp validate_supported_email(%Email{text_body: nil, html_body: nil}),
    do: {:error, :body_not_set}

  defp validate_supported_email(%Email{headers: headers}) when map_size(headers) > @max_headers,
    do: {:error, {:too_many_headers, @max_headers}}

  defp validate_supported_email(%Email{provider_options: %{idempotence_key: key}})
       when not is_binary(key) or key == "",
       do: {:error, :invalid_idempotence_key}

  defp validate_supported_email(%Email{provider_options: %{idempotence_key: key}})
       when byte_size(key) > @max_idempotence_key_length,
       do: {:error, {:idempotence_key_too_long, @max_idempotence_key_length}}

  defp validate_supported_email(%Email{}), do: :ok

  defp encode_payload(%Email{} = email, config) do
    payload =
      %{
        "message" =>
          %{
            "recipients" => Enum.map(email.to, &recipient/1),
            "subject" => email.subject,
            "from_email" => elem(email.from, 1),
            "from_name" => elem(email.from, 0),
            "body" => message_body(email),
            "track_links" => 0,
            "track_read" => 0,
            "idempotence_key" => idempotence_key(email)
          }
          |> maybe_put("skip_unsubscribe", 1, Keyword.get(config, :skip_unsubscribe, false))
          |> maybe_put_reply_to(email.reply_to)
          |> maybe_put_headers(email.headers)
      }

    Jason.encode(payload)
  end

  defp recipient({_name, address}), do: %{"email" => address}

  defp message_body(%Email{text_body: text_body, html_body: html_body}) do
    %{}
    |> maybe_put("plaintext", text_body)
    |> maybe_put("html", html_body)
  end

  defp maybe_put_reply_to(message, nil), do: message

  defp maybe_put_reply_to(message, {name, address}) do
    message
    |> Map.put("reply_to", address)
    |> maybe_put("reply_to_name", name)
  end

  defp maybe_put_headers(message, headers) do
    allowed_headers =
      Map.new(headers, fn {name, value} -> {to_string(name), to_string(value)} end)
      |> Map.filter(fn {name, _value} -> String.starts_with?(String.downcase(name), "x-") end)

    maybe_put(message, "headers", allowed_headers, allowed_headers != %{})
  end

  defp maybe_put(map, _key, nil), do: map
  defp maybe_put(map, key, value), do: Map.put(map, key, value)
  defp maybe_put(map, key, value, true), do: Map.put(map, key, value)
  defp maybe_put(map, _key, _value, false), do: map

  defp idempotence_key(%Email{provider_options: %{idempotence_key: key}})
       when is_binary(key) and key != "",
       do: key

  defp idempotence_key(_email) do
    :crypto.strong_rand_bytes(24)
    |> Base.url_encode64(padding: false)
  end

  defp request_headers(api_key) do
    [
      {"Accept", "application/json"},
      {"Content-Type", "application/json"},
      {"X-API-KEY", api_key}
    ]
  end

  defp endpoint(config) do
    config
    |> Keyword.get(:base_url, @default_base_url)
    |> String.trim_trailing("/")
    |> Kernel.<>(@send_path)
  end

  defp with_client_options(email, config) do
    configured_options = Keyword.get(config, :client_options, [])
    existing_options = Map.get(email.private, :client_options, [])

    Swoosh.Email.put_private(
      email,
      :client_options,
      Keyword.merge(configured_options, existing_options)
    )
  end

  defp decode_response(200, response_body) do
    case Jason.decode(response_body) do
      {:ok, %{"status" => "success"} = response} -> success_response(response)
      {:ok, response} -> {:error, api_error(200, response)}
      {:error, _reason} -> {:error, {:invalid_unisender_go_response, 200}}
    end
  end

  defp decode_response(status, response_body) do
    case Jason.decode(response_body) do
      {:ok, response} -> {:error, api_error(status, response)}
      {:error, _reason} -> {:error, {:invalid_unisender_go_response, status}}
    end
  end

  defp success_response(%{"failed_emails" => failed_emails})
       when is_map(failed_emails) and map_size(failed_emails) > 0 do
    reasons = failed_emails |> Map.values() |> Enum.frequencies()
    {:error, {:unisender_go_recipient_rejected, reasons}}
  end

  defp success_response(response) do
    {:ok,
     %{
       provider: :unisender_go,
       job_id: Map.get(response, "job_id"),
       accepted_count: response |> Map.get("emails", []) |> length()
     }}
  end

  defp api_error(status, response) when is_map(response) do
    {:unisender_go_api_error, status, Map.get(response, "code")}
  end

  defp api_error(status, _response), do: {:unisender_go_api_error, status, nil}

  defp validate_api_key!(api_key) when is_binary(api_key) do
    if String.trim(api_key) == "" do
      raise ArgumentError, "UniSender Go API key is missing"
    end
  end

  defp validate_api_key!(_api_key) do
    raise ArgumentError, "UniSender Go API key is missing"
  end

  defp validate_base_url!(base_url) when is_binary(base_url) do
    uri = URI.parse(base_url)

    unless uri.scheme == "https" and is_binary(uri.host) and uri.host != "" and
             is_binary(uri.path) and uri.path != "" and is_nil(uri.userinfo) and
             is_nil(uri.query) and is_nil(uri.fragment) do
      raise ArgumentError, "UniSender Go base URL must be an HTTPS URL without credentials"
    end
  end

  defp validate_base_url!(_base_url) do
    raise ArgumentError, "UniSender Go base URL must be an HTTPS URL without credentials"
  end

  defp validate_skip_unsubscribe!(value) when is_boolean(value), do: :ok

  defp validate_skip_unsubscribe!(_value) do
    raise ArgumentError, "UniSender Go skip_unsubscribe must be a boolean"
  end
end
