defmodule GlimeshWeb.UserAuth do
  import Plug.Conn
  import Phoenix.Controller

  use GlimeshWeb, :verified_routes

  alias Glimesh.Accounts
  alias GlimeshWeb.Endpoint

  # Make the remember me cookie valid for 60 days.
  # If you want bump or reduce this value, also change
  # the token expiry itself in UserToken.
  @max_age 60 * 60 * 24 * 60
  @remember_me_cookie "user_remember_me"
  @remember_me_options [sign: true, max_age: @max_age]

  @doc """
  Logs the user in.

  It renews the session ID and clears the whole session
  to avoid fixation attacks. See the renew_session
  function to customize this behaviour.

  It also sets a `:live_socket_id` key in the session,
  so LiveView sessions are identified and automatically
  disconnected on log out. The line can be safely removed
  if you are not using LiveView.
  """
  def log_in_user(conn, user, params \\ %{}) do
    token = Accounts.generate_user_session_token(user)
    user_return_to = get_session(conn, :user_return_to)
    last_path = NavigationHistory.last_path(conn)

    user_preference = get_user_preference(user)

    user_ip = conn.remote_ip |> :inet.ntoa() |> to_string()
    {:ok, _} = Accounts.update_user_ip(user, user_ip)

    conn
    |> renew_session()
    |> put_session(:user_token, token)
    |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
    |> put_session(:locale, user_preference.locale)
    |> put_session(:site_theme, user_preference.site_theme)
    |> maybe_write_remember_me_cookie(token, params)
    |> redirect(to: user_return_to || last_path || signed_in_path(conn))
  end

  defp get_user_preference(%Glimesh.Accounts.User{user_preference: user_preference} = user) do
    case user_preference do
      %Glimesh.Accounts.UserPreference{} = pref ->
        pref

      _ ->
        Glimesh.Accounts.get_user_preference!(user)
    end
  end

  defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
    put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
  end

  defp maybe_write_remember_me_cookie(conn, _token, _params) do
    conn
  end

  # This function renews the session ID and erases the whole
  # session to avoid fixation attacks. If there is any data
  # in the session you may want to preserve after log in/log out,
  # you must explicitly fetch the session data before clearing
  # and then immediately set it after clearing, for example:
  #
  #     defp renew_session(conn) do
  #       preferred_locale = get_session(conn, :preferred_locale)
  #
  #       conn
  #       |> configure_session(renew: true)
  #       |> clear_session()
  #       |> put_session(:preferred_locale, preferred_locale)
  #     end
  #
  defp renew_session(conn) do
    conn
    |> configure_session(renew: true)
    |> clear_session()
  end

  @doc """
  Logs the user out.

  It clears all session data for safety. See renew_session.
  """
  def log_out_user(conn) do
    user_token = get_session(conn, :user_token)
    last_path = NavigationHistory.last_path(conn, default: "/")
    user_token && Accounts.delete_session_token(user_token)

    if live_socket_id = get_session(conn, :live_socket_id) do
      Endpoint.broadcast(live_socket_id, "disconnect", %{})
    end

    conn
    |> renew_session()
    |> delete_resp_cookie(@remember_me_cookie)
    |> redirect(to: last_path)
  end

  def ban_user(conn) do
    user_token = get_session(conn, :user_token)
    user_token && Accounts.delete_session_token(user_token)

    if live_socket_id = get_session(conn, :live_socket_id) do
      Endpoint.broadcast(live_socket_id, "disconnect", %{})
    end

    conn
    |> renew_session()
    |> delete_resp_cookie(@remember_me_cookie)
  end

  @doc """
  Authenticates the user by looking into the session
  and remember me token.
  """
  def fetch_current_user(conn, _opts) do
    {user_token, conn} = ensure_user_token(conn)
    user = user_token && Accounts.get_user_by_session_token(user_token)
    assign(conn, :current_user, user)
  end

  defp ensure_user_token(conn) do
    if user_token = get_session(conn, :user_token) do
      {user_token, conn}
    else
      conn = fetch_cookies(conn, signed: [@remember_me_cookie])

      if user_token = conn.cookies[@remember_me_cookie] do
        {user_token, put_session(conn, :user_token, user_token)}
      else
        {nil, conn}
      end
    end
  end

  @doc """
  Used for routes that require the user to not be authenticated.
  """
  def redirect_if_user_is_authenticated(conn, _opts) do
    if conn.assigns[:current_user] do
      conn
      |> redirect(to: signed_in_path(conn))
      |> halt()
    else
      conn
    end
  end

  @doc """
  Used for routes that require the user to be authenticated.

  If you want to enforce the user e-mail is confirmed before
  they use the application at all, here would be a good place.
  """
  def require_authenticated_user(conn, _opts) do
    if conn.assigns[:current_user] do
      conn
    else
      conn
      |> put_flash(:error, "You must log in to access this page.")
      |> maybe_store_return_to()
      |> redirect(to: ~p"/users/log_in")
      |> halt()
    end
  end

  @doc """
  Used for routes that require the user to have a channel.
  """
  def require_user_has_channel(conn, _opts) do
    if Map.has_key?(conn.assigns, :current_user) and
         Glimesh.ChannelLookups.get_channel_for_user(conn.assigns[:current_user]) do
      conn
    else
      conn
      |> put_flash(:error, "You must have a channel to access this page.")
      |> redirect(to: ~p"/users/settings/stream")
      |> halt()
    end
  end

  @doc """
  Used for routes that require the user to be an administrator.
  """
  def require_admin_user(conn, _opts) do
    if conn.assigns[:current_user] && conn.assigns[:current_user].is_admin do
      conn
    else
      conn
      |> put_flash(:error, "You must be an administrator to access this page.")
      |> redirect(to: ~p"/users/log_in")
      |> halt()
    end
  end

  def require_gct_user(conn, _opts) do
    if conn.assigns[:current_user] && conn.assigns[:current_user].is_gct &&
         conn.assigns[:current_user].tfa_token do
      conn
    else
      conn
      |> redirect(to: ~p"/users/log_in")
      |> halt()
    end
  end

  @doc """
  Used for routes that require the user to be an events team member.
  """
  def require_events_team_user(conn, _opts) do
    if conn.assigns[:current_user] && conn.assigns[:current_user].is_events_team do
      conn
    else
      conn
      |> redirect(to: ~p"/users/log_in")
      |> halt()
    end
  end

  @doc """
  Used for api routes that require authentication.
  """
  def require_authenticated_user_api(conn, _opts) do
    if conn.assigns[:current_user] do
      conn
    else
      conn
      |> json(%{error: "You must be logged in to access the api"})
      |> halt()
    end
  end

  def require_alpha_api_header(conn, _opts) do
    value = Application.get_env(:glimesh, :alpha_api_header, nil)

    cond do
      Application.get_env(:glimesh, :alpha_api_enable, false) ->
        conn

      conn.assigns[:current_user] && conn.assigns[:current_user].is_admin ->
        conn

      !is_nil(value) and get_req_header(conn, "glimesh-alpha-api") == value ->
        conn

      true ->
        conn
        |> json(%{error: "Sorry, our new API is not available for testing yet!"})
        |> halt()
    end
  end

  defp maybe_store_return_to(%{method: "GET", request_path: request_path} = conn) do
    maybe_query_string =
      case conn.query_string do
        "" -> ""
        _ -> "?#{conn.query_string}"
      end

    put_session(conn, :user_return_to, request_path <> maybe_query_string)
  end

  defp maybe_store_return_to(conn), do: conn

  defp signed_in_path(_conn), do: "/"
end
