defmodule Berrypod.Mailer do
  use Swoosh.Mailer, otp_app: :berrypod

  alias Berrypod.KeyValidation
  alias Berrypod.Mailer.Adapters
  alias Berrypod.Settings

  @doc """
  Returns whether a real email adapter is configured.

  True when the adapter is anything other than `Swoosh.Adapters.Local`
  (which just stores emails in memory for dev use).
  """
  def email_configured? do
    adapter = Application.get_env(:berrypod, __MODULE__)[:adapter]
    adapter != nil and adapter != Swoosh.Adapters.Local
  end

  @doc """
  Returns whether email delivery has been verified via a successful test email.

  This is the flag the login page uses to decide whether to show the magic link
  form. A configured adapter alone isn't enough — the admin must have sent a
  test email that succeeded.
  """
  def email_verified? do
    email_configured?() and Settings.get_setting("email_verified", false) == true
  end

  @doc "Marks email delivery as verified (called after a successful test email)."
  def mark_email_verified do
    Settings.put_setting("email_verified", true, "boolean")
  end

  @doc "Clears the email verified flag (called when config changes)."
  def clear_email_verified do
    Settings.delete_setting("email_verified")
  end

  @doc """
  Returns true if email is configured via environment variables (SMTP_HOST).

  When env vars are active, the admin UI shows the config as read-only.
  """
  def env_var_configured? do
    System.get_env("SMTP_HOST") != nil
  end

  @doc """
  Returns the current adapter key and config for display in the admin UI.

  Returns `{adapter_key, config_map}` or `{nil, %{}}` if using the default.
  """
  def current_config do
    mailer_config = Application.get_env(:berrypod, __MODULE__, [])
    adapter = mailer_config[:adapter]

    case Enum.find(Adapters.all(), &(&1.module == adapter)) do
      nil ->
        {nil, %{}}

      adapter_info ->
        config =
          for field <- adapter_info.fields, into: %{} do
            settings_key = Adapters.settings_key(adapter_info.key, field.key)

            value =
              case field.type do
                :secret -> Settings.secret_hint(settings_key)
                _ -> Settings.get_setting(settings_key)
              end

            {field.key, value}
          end

        {adapter_info.key, config}
    end
  end

  @doc """
  Loads email config from the Settings table and applies it to Application env.

  Env vars take precedence — if SMTP_HOST is set, this is a no-op since
  runtime.exs already configured the adapter.

  Called on boot (from Application.start) and after admin saves email settings.
  """
  def load_config do
    if env_var_configured?() do
      :ok
    else
      case Settings.get_setting("email_adapter") do
        nil ->
          :ok

        adapter_key ->
          case Adapters.get(adapter_key) do
            nil ->
              :ok

            adapter_info ->
              config = build_config(adapter_info)
              Application.put_env(:berrypod, __MODULE__, config)
              # API-based adapters need a real HTTP client (dev defaults to false)
              if adapter_info.module != Swoosh.Adapters.SMTP do
                Application.put_env(:swoosh, :api_client, Swoosh.ApiClient.Req)
              end

              :ok
          end
      end
    end
  end

  @doc """
  Sends a test email to the given address using the current config.
  """
  def send_test_email(to_address, from \\ nil) do
    import Swoosh.Email

    email =
      new()
      |> to(to_address)
      |> from({"Berrypod", from || from_address()})
      |> subject("Berrypod test email")
      |> text_body("This is a test email from your Berrypod shop. Email delivery is working.")

    deliver(email)
  end

  @doc "Returns the configured from address for outbound email."
  def from_address do
    Settings.get_setting("email_from_address") || "noreply@#{default_from_domain()}"
  end

  @doc """
  Validates and persists email adapter configuration.

  Trims values, validates required fields and key formats, clears settings
  from other providers, and applies config immediately.

  Returns `{:ok, adapter_info}` on success or `{:error, field_errors}`
  where field_errors is a map of field_key => error_message.
  """
  def save_config(adapter_key, params, fallback_from_email) do
    case Adapters.get(adapter_key) do
      nil ->
        {:error, %{"_base" => "Please select an email provider"}}

      adapter_info ->
        params = trim_params(params)
        field_errors = validate_adapter_fields(adapter_info, params)

        if field_errors == %{} do
          persist_adapter_config(adapter_info, params, fallback_from_email)
          {:ok, adapter_info}
        else
          {:error, field_errors}
        end
    end
  end

  defp trim_params(params) do
    Map.new(params, fn {k, v} -> {k, if(is_binary(v), do: String.trim(v), else: v)} end)
  end

  defp validate_adapter_fields(adapter_info, params) do
    missing_errors =
      for field <- adapter_info.fields,
          field.required,
          val = params[field.key],
          is_nil(val) or val == "",
          # Secret fields can be left blank to keep existing value
          not (field.type == :secret and
                 Settings.get_secret(Adapters.settings_key(adapter_info.key, field.key)) != nil),
          into: %{} do
        {field.key, "#{field.label} is required"}
      end

    format_errors =
      for field <- adapter_info.fields,
          field.type == :secret,
          value = params[field.key],
          value != nil and value != "",
          {:error, message} <- [
            KeyValidation.validate_email_key(value, adapter_info.key, field.key)
          ],
          into: %{} do
        {field.key, message}
      end

    integer_errors =
      for field <- adapter_info.fields,
          field.type == :integer,
          value = params[field.key],
          is_binary(value) and value != "",
          match?(:error, Integer.parse(value)),
          into: %{} do
        {field.key, "#{field.label} must be a number"}
      end

    missing_errors |> Map.merge(format_errors) |> Map.merge(integer_errors)
  end

  defp persist_adapter_config(adapter_info, params, fallback_from_email) do
    # Clear settings from other providers
    new_keys = MapSet.new(Adapters.field_keys(adapter_info))

    for key <- Adapters.all_field_keys(), key not in new_keys do
      Settings.delete_setting(key)
    end

    # Save adapter type
    Settings.put_setting("email_adapter", adapter_info.key)

    # Save field values (blank secrets keep existing value)
    for field <- adapter_info.fields do
      value = params[field.key]
      settings_key = Adapters.settings_key(adapter_info.key, field.key)

      cond do
        value && value != "" ->
          case field.type do
            :secret -> Settings.put_secret(settings_key, value)
            :integer -> Settings.put_setting(settings_key, String.to_integer(value), "integer")
            _ -> Settings.put_setting(settings_key, value)
          end

        field.type == :secret ->
          :keep

        true ->
          Settings.delete_setting(settings_key)
      end
    end

    # Auto-set from address to admin email if not configured
    if Settings.get_setting("email_from_address") in [nil, ""] do
      Settings.put_setting("email_from_address", fallback_from_email)
    end

    # Require re-verification and apply immediately
    clear_email_verified()
    load_config()
  end

  @doc """
  Turns a raw delivery error into a user-friendly message.

  Swoosh adapters return varied error shapes — HTTP status tuples,
  SMTP failure atoms, network errors, etc. This normalises them into
  a single readable string for the admin UI.
  """
  def friendly_error({status, body}) when is_integer(status) do
    message = extract_message(body)

    case status do
      401 ->
        "Your API key was rejected" <>
          if(message, do: " — #{message}", else: ". Double-check you pasted the right key")

      403 ->
        "Your API key doesn't have permission to send email" <>
          if(message, do: " — #{message}", else: "")

      404 ->
        "Provider endpoint not found" <>
          if(message, do: " — #{message}", else: ". Check your config")

      422 ->
        "The provider rejected the request" <> if(message, do: " — #{message}", else: "")

      429 ->
        "Rate limit reached. Wait a moment and try again"

      status when status >= 500 ->
        "The email provider is having issues (#{status}). Try again in a few minutes"

      _ ->
        "Provider returned error #{status}" <> if(message, do: " — #{message}", else: "")
    end
  end

  # SMTP errors: {:permanent_failure, message} or {:temporary_failure, message}
  def friendly_error({:permanent_failure, msg}) when is_binary(msg),
    do: "SMTP server rejected the email: #{truncate(msg)}"

  def friendly_error({:temporary_failure, msg}) when is_binary(msg),
    do: "SMTP server temporarily unavailable: #{truncate(msg)}"

  def friendly_error({:retries_exceeded, _}),
    do: "SMTP connection failed after retries. Check your server address and port"

  # SMTP connection errors
  def friendly_error({:no_more_hosts, _}),
    do: "Couldn't connect to the SMTP server. Check the hostname and port"

  def friendly_error({:auth_failed, _}), do: "SMTP login failed. Check your username and password"

  # Network-level errors
  def friendly_error(:timeout), do: "Connection timed out. Check your internet connection"
  def friendly_error(:econnrefused), do: "Connection refused. Check the server address and port"
  def friendly_error(:nxdomain), do: "Server hostname not found. Check the address"
  def friendly_error(:closed), do: "Connection was closed. The server may be down"

  # Catch-all
  def friendly_error(other) when is_binary(other), do: other
  def friendly_error(other), do: "Delivery failed: #{inspect(other)}"

  @doc """
  Returns whether a delivery error is transient (worth retrying).

  Config errors (bad key, wrong permissions) need a fix, not a retry.
  Transient errors (rate limits, timeouts, server issues) may resolve on their own.
  """
  def retryable_error?({status, _}) when is_integer(status), do: status in [429] or status >= 500
  def retryable_error?({:temporary_failure, _}), do: true
  def retryable_error?({:retries_exceeded, _}), do: true
  def retryable_error?(:timeout), do: true
  def retryable_error?(:closed), do: true
  def retryable_error?(_), do: false

  # Extract a human-readable message from various API error body shapes
  defp extract_message(%{"message" => msg}) when is_binary(msg), do: downcase_first(msg)
  defp extract_message(%{"ErrorMessage" => msg}) when is_binary(msg), do: downcase_first(msg)

  defp extract_message(%{"error" => %{"message" => msg}}) when is_binary(msg),
    do: downcase_first(msg)

  defp extract_message(%{"errors" => [%{"message" => msg} | _]}) when is_binary(msg),
    do: downcase_first(msg)

  defp extract_message(body) when is_binary(body) and byte_size(body) < 200, do: body
  defp extract_message(_), do: nil

  defp downcase_first(<<first::utf8, rest::binary>>), do: String.downcase(<<first::utf8>>) <> rest
  defp downcase_first(str), do: str

  defp truncate(msg) when byte_size(msg) > 120, do: binary_part(msg, 0, 120) <> "..."
  defp truncate(msg), do: msg

  # Build Swoosh config keyword list from Settings for a given adapter
  defp build_config(adapter_info) do
    opts =
      for field <- adapter_info.fields, reduce: [] do
        acc ->
          settings_key = Adapters.settings_key(adapter_info.key, field.key)

          value =
            case field.type do
              :secret -> Settings.get_secret(settings_key)
              _ -> Settings.get_setting(settings_key)
            end

          case {value, field} do
            {nil, _} ->
              acc

            {val, %{type: :integer}} ->
              [{String.to_atom(field.key), to_integer(val)} | acc]

            {val, _} ->
              [{String.to_atom(field.key), val} | acc]
          end
      end

    # SMTP uses :relay, others use the native Swoosh key names
    [{:adapter, adapter_info.module} | opts]
  end

  defp to_integer(val) when is_integer(val), do: val
  defp to_integer(val) when is_binary(val), do: String.to_integer(val)

  defp default_from_domain do
    case Application.get_env(:berrypod, BerrypodWeb.Endpoint)[:url][:host] do
      nil -> "example.com"
      host -> host
    end
  end
end
