require Logger

defmodule MvWeb.AuthController do
  @moduledoc """
  Handles authentication callbacks for password and OIDC authentication.

  This controller manages:
  - Successful authentication (password, OIDC, password reset, email confirmation)
  - Authentication failures with appropriate error handling
  - OIDC account linking flow when email collision occurs
  - Sign out functionality
  """

  use MvWeb, :controller
  use AshAuthentication.Phoenix.Controller

  alias Mv.Accounts.User.Errors.PasswordVerificationRequired

  def success(conn, activity, user, _token) do
    return_to = get_session(conn, :return_to) || ~p"/"

    message =
      case activity do
        {:confirm_new_user, :confirm} -> gettext("Your email address has now been confirmed")
        {:password, :reset} -> gettext("Your password has successfully been reset")
        _ -> gettext("You are now signed in")
      end

    conn
    |> delete_session(:return_to)
    |> store_in_session(user)
    # If your resource has a different name, update the assign name here (i.e :current_admin)
    |> assign(:current_user, user)
    |> put_flash(:success, message)
    |> redirect(to: return_to)
  end

  @doc """
  Handles authentication failures and routes to appropriate error handling.

  Manages:
  - OIDC email collisions (triggers password verification flow)
  - Generic OIDC authentication failures
  - Unconfirmed account errors
  - Generic authentication failures
  """
  def failure(conn, activity, reason) do
    log_failure_safely(activity, reason)

    case {activity, reason} do
      {{:oidc, _action}, reason} ->
        handle_oidc_failure(conn, reason)

      {_, %AshAuthentication.Errors.AuthenticationFailed{caused_by: caused_by}} ->
        handle_authentication_failed(conn, caused_by)

      _ ->
        conn
        |> put_flash(:error, gettext("Incorrect email or password"))
        |> redirect(to: ~p"/sign-in")
    end
  end

  # Log authentication failures safely, avoiding sensitive data for {:oidc, _} activities
  defp log_failure_safely({:oidc, _action} = activity, reason) do
    # For Assent errors, use safe_assent_meta to avoid logging tokens/URLs with query params
    case reason do
      %Assent.ServerUnreachableError{} = err ->
        meta = safe_assent_meta(err)
        message = format_safe_log_message("Authentication failure", activity, meta)
        Logger.warning(message)

      %Assent.InvalidResponseError{} = err ->
        meta = safe_assent_meta(err)
        message = format_safe_log_message("Authentication failure", activity, meta)
        Logger.warning(message)

      _ ->
        # For other OIDC errors, log only error type, not full details
        error_type = get_error_type(reason)

        Logger.warning(
          "Authentication failure - Activity: #{inspect(activity)}, Error type: #{error_type}"
        )
    end
  end

  defp log_failure_safely(activity, reason) do
    # For non-OIDC activities, safe to log full reason
    Logger.warning(
      "Authentication failure - Activity: #{inspect(activity)}, Reason: #{inspect(reason)}"
    )
  end

  # Extract safe error type identifier without sensitive data
  defp get_error_type(%struct{}), do: "#{struct}"
  defp get_error_type(atom) when is_atom(atom), do: inspect(atom)
  defp get_error_type(_other), do: "[redacted]"

  # Format safe log message with metadata included in the message string
  defp format_safe_log_message(base_message, activity, meta) when is_list(meta) do
    activity_str = "Activity: #{inspect(activity)}"
    meta_str = format_meta_string(meta)
    "#{base_message} - #{activity_str}#{meta_str}"
  end

  defp format_meta_string([]), do: ""

  defp format_meta_string(meta) when is_list(meta) do
    parts =
      Enum.map(meta, fn
        {:request_url, url} -> "Request URL: #{url}"
        {:status, status} -> "Status: #{status}"
        {:http_adapter, adapter} -> "HTTP Adapter: #{inspect(adapter)}"
        _ -> nil
      end)
      |> Enum.filter(&(&1 != nil))

    if Enum.empty?(parts), do: "", else: " - " <> Enum.join(parts, ", ")
  end

  # Handle all OIDC authentication failures
  defp handle_oidc_failure(conn, %Ash.Error.Invalid{errors: errors}) do
    handle_oidc_email_collision(conn, errors)
  end

  defp handle_oidc_failure(conn, %AshAuthentication.Errors.AuthenticationFailed{
         caused_by: caused_by
       }) do
    case caused_by do
      %Ash.Error.Invalid{errors: errors} ->
        handle_oidc_email_collision(conn, errors)

      _ ->
        conn
        |> put_flash(:error, gettext("Unable to authenticate with OIDC. Please try again."))
        |> redirect(to: ~p"/sign-in")
    end
  end

  # Handle Assent server unreachable errors (network/connectivity issues)
  defp handle_oidc_failure(conn, %Assent.ServerUnreachableError{} = _err) do
    # Logging already done safely in failure/3 via log_failure_safely/2
    # No need to log again here to avoid duplicate logs

    conn
    |> put_flash(
      :error,
      gettext("The authentication server is currently unavailable. Please try again later.")
    )
    |> redirect(to: ~p"/sign-in")
  end

  # Handle Assent invalid response errors (configuration or malformed responses)
  defp handle_oidc_failure(conn, %Assent.InvalidResponseError{} = _err) do
    # Logging already done safely in failure/3 via log_failure_safely/2
    # No need to log again here to avoid duplicate logs

    conn
    |> put_flash(
      :error,
      gettext("Authentication configuration error. Please contact the administrator.")
    )
    |> redirect(to: ~p"/sign-in")
  end

  # Catch-all clause for any other error types
  defp handle_oidc_failure(conn, _reason) do
    # Logging already done safely in failure/3 via log_failure_safely/2
    # No need to log again here to avoid duplicate logs

    conn
    |> put_flash(:error, gettext("Unable to authenticate with OIDC. Please try again."))
    |> redirect(to: ~p"/sign-in")
  end

  # Handle generic AuthenticationFailed errors
  defp handle_authentication_failed(conn, %Ash.Error.Forbidden{errors: errors}) do
    if Enum.any?(errors, &match?(%AshAuthentication.Errors.CannotConfirmUnconfirmedUser{}, &1)) do
      message =
        gettext("""
        You have already signed in another way, but have not confirmed your account.
        You can confirm your account using the link we sent to you, or by resetting your password.
        """)

      conn
      |> put_flash(:error, message)
      |> redirect(to: ~p"/sign-in")
    else
      conn
      |> put_flash(:error, gettext("Authentication failed. Please try again."))
      |> redirect(to: ~p"/sign-in")
    end
  end

  defp handle_authentication_failed(conn, _other) do
    conn
    |> put_flash(:error, gettext("Authentication failed. Please try again."))
    |> redirect(to: ~p"/sign-in")
  end

  # Handle OIDC email collision - user needs to verify password to link accounts
  defp handle_oidc_email_collision(conn, errors) do
    case find_password_verification_error(errors) do
      %PasswordVerificationRequired{user_id: user_id, oidc_user_info: oidc_user_info} ->
        redirect_to_account_linking(conn, user_id, oidc_user_info)

      nil ->
        # Check if it's a "different OIDC account" error or email uniqueness error
        error_message = extract_meaningful_error_message(errors)

        conn
        |> put_flash(:error, error_message)
        |> redirect(to: ~p"/sign-in")
    end
  end

  # Extract meaningful error message from Ash errors
  defp extract_meaningful_error_message(errors) do
    # Look for specific error messages in InvalidAttribute errors
    meaningful_error =
      Enum.find_value(errors, fn
        %Ash.Error.Changes.InvalidAttribute{message: message, field: :email}
        when is_binary(message) ->
          cond do
            # Email update conflict during OIDC login
            String.contains?(message, "Cannot update email to") and
                String.contains?(message, "already registered to another account") ->
              gettext(
                "Cannot update email: This email is already registered to another account. Please change your email in the identity provider."
              )

            # Different OIDC account error
            String.contains?(message, "already linked to a different OIDC account") ->
              gettext(
                "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
              )

            true ->
              nil
          end

        %Ash.Error.Changes.InvalidAttribute{message: message}
        when is_binary(message) ->
          # Return any other meaningful message
          if String.length(message) > 20 and
               not String.contains?(message, "has already been taken") do
            message
          else
            nil
          end

        _ ->
          nil
      end)

    meaningful_error || gettext("Unable to sign in. Please try again.")
  end

  # Find PasswordVerificationRequired error in error list
  defp find_password_verification_error(errors) do
    Enum.find(errors, &match?(%PasswordVerificationRequired{}, &1))
  end

  # Redirect to account linking page with OIDC info stored in session
  defp redirect_to_account_linking(conn, user_id, oidc_user_info) do
    conn
    |> put_session(:oidc_linking_user_id, user_id)
    |> put_session(:oidc_linking_user_info, oidc_user_info)
    |> put_flash(
      :info,
      gettext(
        "An account with this email already exists. Please verify your password to link your OIDC account."
      )
    )
    |> redirect(to: ~p"/auth/link-oidc-account")
  end

  # Extract safe metadata from Assent errors for logging
  # Never logs sensitive data: no tokens, secrets, or full request URLs
  # Returns keyword list for Logger.warning/2
  defp safe_assent_meta(%{request_url: url} = err) when is_binary(url) do
    [
      request_url: redact_url(url),
      http_adapter: Map.get(err, :http_adapter)
    ]
    |> Enum.filter(fn {_key, value} -> not is_nil(value) end)
  end

  # Handle InvalidResponseError which has :response field (HTTPResponse struct)
  defp safe_assent_meta(%{response: %{status: status} = response} = err) do
    [
      status: status,
      http_adapter: Map.get(response, :http_adapter) || Map.get(err, :http_adapter)
    ]
    |> Enum.filter(fn {_key, value} -> not is_nil(value) end)
  end

  defp safe_assent_meta(err) do
    # Only extract safe, simple fields
    [
      http_adapter: Map.get(err, :http_adapter)
    ]
    |> Enum.filter(fn {_key, value} -> not is_nil(value) end)
  end

  # Redact URL to only show scheme and host, hiding path, query, and fragments
  defp redact_url(url) when is_binary(url) do
    case URI.parse(url) do
      %URI{scheme: scheme, host: host} when not is_nil(scheme) and not is_nil(host) ->
        "#{scheme}://#{host}"

      _ ->
        "[redacted]"
    end
  end

  defp redact_url(_), do: "[redacted]"

  def sign_out(conn, _params) do
    return_to = get_session(conn, :return_to) || ~p"/"

    conn
    |> clear_session(:mv)
    |> put_flash(:success, gettext("You are now signed out"))
    |> redirect(to: return_to)
  end
end
