defmodule PlausibleWeb.AuthController do
  use PlausibleWeb, :controller
  use Plausible.Repo
  use Plausible

  alias Plausible.Auth
  alias Plausible.Teams
  alias PlausibleWeb.TwoFactor
  alias PlausibleWeb.UserAuth
  alias PlausibleWeb.LoginPreference

  require Logger

  plug(
    PlausibleWeb.RequireLoggedOutPlug
    when action in [
           :register,
           :register_from_invitation,
           :login_form,
           :login,
           :verify_2fa_form,
           :verify_2fa,
           :verify_2fa_recovery_code_form,
           :verify_2fa_recovery_code
         ]
  )

  plug(
    PlausibleWeb.RequireAccountPlug
    when action in [
           :delete_me,
           :activate_form,
           :activate,
           :request_activation_code,
           :initiate_2fa_setup,
           :verify_2fa_setup_form,
           :verify_2fa_setup,
           :disable_2fa,
           :generate_2fa_recovery_codes,
           :select_team,
           :switch_team
         ]
  )

  plug Plausible.Plugs.RestrictUserType,
       [deny: :sso] when action in [:delete_me, :disable_2fa]

  plug(
    :clear_2fa_user
    when action not in [
           :verify_2fa_form,
           :verify_2fa,
           :verify_2fa_recovery_code_form,
           :verify_2fa_recovery_code
         ]
  )

  # Plug purging 2FA user session cookie outsite 2FA flow
  defp clear_2fa_user(conn, _opts) do
    TwoFactor.Session.clear_2fa_user(conn)
  end

  def select_team(conn, _params) do
    current_user = conn.assigns.current_user
    current_team = conn.assigns[:current_team]

    owner_name_fn = fn owner ->
      if owner.id == current_user.id do
        "You"
      else
        owner.name
      end
    end

    teams =
      current_user
      |> Teams.Users.teams()
      |> Enum.filter(& &1.setup_complete)
      |> Enum.map(fn team ->
        current_team? = current_team && team.id == current_team.id

        owners =
          Enum.map_join(team.owners, ", ", &owner_name_fn.(&1))

        many_owners? = length(team.owners) > 1

        %{
          identifier: team.identifier,
          name: team.name,
          current?: current_team?,
          many_owners?: many_owners?,
          owners: owners
        }
      end)

    case teams do
      [] ->
        redirect(conn, to: Routes.site_path(conn, :index))

      [%{identifier: sole_team_identifier}] ->
        redirect(conn, to: Routes.site_path(conn, :index, __team: sole_team_identifier))

      [_ | _] ->
        render(conn, "select_team.html", teams_selection: teams)
    end
  end

  def activate_form(conn, params) do
    user = conn.assigns.current_user
    flow = params["flow"] || PlausibleWeb.Flows.register()
    team_identifier = params["team_identifier"]

    render(conn, "activate.html",
      error: nil,
      has_email_code?: Plausible.Users.has_email_code?(user),
      has_any_memberships?: Plausible.Teams.Users.has_sites?(user),
      form_submit_url: "/activate?flow=#{flow}",
      team_identifier: team_identifier
    )
  end

  def activate(conn, %{"code" => code}) do
    user = conn.assigns[:current_user]

    has_any_invitations? = Plausible.Teams.Users.has_sites?(user, include_pending?: true)
    has_any_memberships? = Plausible.Teams.Users.has_sites?(user, include_pending?: false)

    flow = conn.params["flow"]
    team_identifier = conn.params["team_identifier"]

    case Auth.EmailVerification.verify_code(user, code) do
      :ok ->
        cond do
          team_identifier not in ["", nil] ->
            redirect_path = accept_team_invitation(conn, team_identifier, user, flow: flow)
            redirect(conn, to: redirect_path)

          has_any_memberships? ->
            handle_email_updated(conn)

          has_any_invitations? ->
            redirect_path = accept_team_invitation(conn, team_identifier, user, flow: flow)
            redirect(conn, to: redirect_path)

          true ->
            redirect(conn, to: Routes.site_path(conn, :new, flow: flow))
        end

      {:error, :incorrect} ->
        render(conn, "activate.html",
          error: "Incorrect activation code",
          has_email_code?: true,
          has_any_memberships?: has_any_memberships?,
          form_submit_url: "/activate?flow=#{flow}"
        )

      {:error, :expired} ->
        render(conn, "activate.html",
          error: "Code is expired, please request another one",
          has_email_code?: false,
          has_any_memberships?: has_any_memberships?,
          form_submit_url: "/activate?flow=#{flow}"
        )
    end
  end

  def request_activation_code(conn, _params) do
    user = conn.assigns.current_user
    Auth.EmailVerification.issue_code(user)

    conn
    |> put_flash(:success, "Activation code was sent to #{user.email}")
    |> redirect(to: Routes.auth_path(conn, :activate_form))
  end

  def password_reset_request_form(conn, _) do
    render(conn, "password_reset_request_form.html")
  end

  def password_reset_request(conn, %{"email" => ""}) do
    render(conn, "password_reset_request_form.html", error: "Please enter an email address")
  end

  def password_reset_request(conn, %{"email" => email} = params) do
    if PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do
      case Auth.lookup(email) do
        {:ok, _user} ->
          token = Auth.Token.sign_password_reset(email)
          url = PlausibleWeb.Endpoint.url() <> "/password/reset?token=#{token}"
          email_template = PlausibleWeb.Email.password_reset_email(email, url)
          Plausible.Mailer.deliver_later(email_template)

          Logger.debug(
            "Password reset e-mail sent. In dev environment GET /sent-emails for details."
          )

          render(conn, "password_reset_request_success.html", email: email)

        {:error, _} ->
          render(conn, "password_reset_request_success.html", email: email)
      end
    else
      render(conn, "password_reset_request_form.html",
        error: "Please complete the captcha to reset your password"
      )
    end
  end

  def password_reset_form(conn, params) do
    case Auth.Token.verify_password_reset(params["token"]) do
      {:ok, %{email: email}} ->
        render(conn, "password_reset_form.html",
          connect_live_socket: true,
          email: email
        )

      {:error, :expired} ->
        render_error(
          conn,
          401,
          "Your token has expired. Please request another password reset link."
        )

      {:error, _} ->
        render_error(
          conn,
          401,
          "Your token is invalid. Please request another password reset link."
        )
    end
  end

  def password_reset(conn, _params) do
    conn
    |> UserAuth.log_out_user()
    |> put_flash(:login_title, "Password updated successfully")
    |> put_flash(:login_instructions, "Please log in with your new credentials")
    |> redirect(to: Routes.auth_path(conn, :login_form))
  end

  on_ee do
    def login_form(conn, params) do
      login_preference = LoginPreference.get(conn)
      error = Phoenix.Flash.get(conn.assigns.flash, :login_error)

      case {login_preference, params["prefer"], error} do
        {"sso", nil, nil} ->
          redirect(conn, to: Routes.sso_path(conn, :login_form, return_to: params["return_to"]))

        _ ->
          render(conn, "login_form.html")
      end
    end
  else
    def login_form(conn, _params) do
      render(conn, "login_form.html")
    end
  end

  def login(conn, %{"user" => params}) do
    login(conn, params)
  end

  def login(conn, %{"email" => email, "password" => password} = params) do
    with :ok <- Auth.rate_limit(:login_ip, conn),
         {:ok, user} <- Auth.lookup(email),
         :ok <- Auth.rate_limit(:login_user, user),
         :ok <- Auth.check_password(user, password),
         :ok <- check_2fa_verified(conn, user) do
      redirect_path =
        cond do
          not is_nil(params["register_action"]) and not user.email_verified ->
            Auth.EmailVerification.issue_code(user)

            flow =
              if params["register_action"] == "register_form" do
                PlausibleWeb.Flows.register()
              else
                PlausibleWeb.Flows.invitation()
              end

            Routes.auth_path(conn, :activate_form,
              flow: flow,
              team_identifier: params["team_identifier"]
            )

          params["register_action"] == "register_from_invitation_form" ->
            accept_team_invitation(conn, params["team_identifier"], user)

          params["register_action"] == "register_form" ->
            Routes.site_path(conn, :new)

          true ->
            params["return_to"]
        end

      conn
      |> LoginPreference.clear()
      |> UserAuth.log_in_user(user, redirect_path)
    else
      {:error, :wrong_password} ->
        Auth.log_failed_login_attempt("wrong password for #{email}")

        conn
        |> put_flash(:login_error, "Wrong email or password. Please try again.")
        |> render("login_form.html")

      {:error, :user_not_found} ->
        Auth.log_failed_login_attempt("user not found for #{email}")
        Plausible.Auth.Password.dummy_calculation()

        conn
        |> put_flash(:login_error, "Wrong email or password. Please try again.")
        |> render("login_form.html")

      {:error, {:rate_limit, _}} ->
        Auth.log_failed_login_attempt("too many login attempts for #{email}")

        render_error(
          conn,
          429,
          "Too many login attempts. Wait a minute before trying again."
        )

      {:error, {:unverified_2fa, user}} ->
        query_params =
          if params["return_to"] not in [nil, ""], do: [return_to: params["return_to"]], else: []

        conn
        |> TwoFactor.Session.set_2fa_user(user)
        |> redirect(to: Routes.auth_path(conn, :verify_2fa, query_params))
    end
  end

  def login(conn, _params) do
    conn |> send_resp(403, "") |> halt()
  end

  defp accept_team_invitation(conn, team_identifier, user, params \\ [])

  defp accept_team_invitation(conn, no_identifier, _user, params)
       when no_identifier in ["", nil] do
    Routes.site_path(conn, :index, params)
  end

  defp accept_team_invitation(conn, team_identifier, user, extra_params) do
    params = Keyword.merge([__team: team_identifier], extra_params)

    # We try switching to the team no matter the invitation presence or acceptance outcome.
    case Teams.Invitations.find_by_team_identifier(team_identifier, user) do
      {:ok, invitation} ->
        {_, _} = Teams.Invitations.accept_team_invitation(invitation, user)
        Routes.site_path(conn, :index, params)

      {:error, :invitation_not_found} ->
        Routes.site_path(conn, :index, params)
    end
  end

  defp check_2fa_verified(conn, user) do
    if Auth.TOTP.enabled?(user) and not TwoFactor.Session.remember_2fa?(conn, user) do
      {:error, {:unverified_2fa, user}}
    else
      :ok
    end
  end

  def initiate_2fa_setup(conn, _params) do
    case Auth.TOTP.initiate(conn.assigns.current_user) do
      {:ok, user, %{totp_uri: totp_uri, secret: secret}} ->
        render(conn, "initiate_2fa_setup.html", user: user, totp_uri: totp_uri, secret: secret)

      {:error, :already_setup} ->
        conn
        |> put_flash(:error, "Two-Factor Authentication is already setup for this account.")
        |> redirect(to: Routes.settings_path(conn, :security) <> "#update-2fa")
    end
  end

  def verify_2fa_setup_form(conn, _params) do
    if Auth.TOTP.initiated?(conn.assigns.current_user) do
      render(conn, "verify_2fa_setup.html")
    else
      redirect(conn, to: Routes.settings_path(conn, :security) <> "#update-2fa")
    end
  end

  def verify_2fa_setup(conn, %{"code" => code}) do
    case Auth.TOTP.enable(conn.assigns.current_user, code) do
      {:ok, _, %{recovery_codes: codes}} ->
        conn
        |> put_flash(:success, "Two-Factor Authentication is fully enabled")
        |> render("generate_2fa_recovery_codes.html", recovery_codes: codes, from_setup: true)

      {:error, :invalid_code} ->
        conn
        |> put_flash(:error, "The provided code is invalid. Please try again")
        |> render("verify_2fa_setup.html")

      {:error, :not_initiated} ->
        conn
        |> put_flash(:error, "Please enable Two-Factor Authentication for this account first.")
        |> redirect(to: Routes.settings_path(conn, :security) <> "#update-2fa")
    end
  end

  def disable_2fa(conn, %{"password" => password}) do
    case Auth.TOTP.disable(conn.assigns.current_user, password) do
      {:ok, _} ->
        conn
        |> TwoFactor.Session.clear_remember_2fa()
        |> put_flash(:success, "Two-Factor Authentication is disabled")
        |> redirect(to: Routes.settings_path(conn, :security) <> "#update-2fa")

      {:error, :invalid_password} ->
        conn
        |> put_flash(:error, "Incorrect password provided")
        |> redirect(to: Routes.settings_path(conn, :security) <> "#update-2fa")
    end
  end

  def generate_2fa_recovery_codes(conn, %{"password" => password}) do
    case Auth.TOTP.generate_recovery_codes(conn.assigns.current_user, password) do
      {:ok, codes} ->
        conn
        |> put_flash(:success, "New Recovery Codes generated")
        |> render("generate_2fa_recovery_codes.html", recovery_codes: codes, from_setup: false)

      {:error, :invalid_password} ->
        conn
        |> put_flash(:error, "Incorrect password provided")
        |> redirect(to: Routes.settings_path(conn, :security) <> "#update-2fa")

      {:error, :not_enabled} ->
        conn
        |> put_flash(:error, "Please enable Two-Factor Authentication for this account first.")
        |> redirect(to: Routes.settings_path(conn, :security) <> "#update-2fa")
    end
  end

  def verify_2fa_form(conn, _params) do
    case TwoFactor.Session.get_2fa_user(conn) do
      {:ok, user} ->
        if Auth.TOTP.enabled?(user) do
          render(conn, "verify_2fa.html",
            remember_2fa_days: TwoFactor.Session.remember_2fa_days()
          )
        else
          redirect_to_login(conn)
        end

      {:error, :not_found} ->
        redirect_to_login(conn)
    end
  end

  def verify_2fa(conn, %{"code" => code} = params) do
    with {:ok, user} <- get_2fa_user_limited(conn) do
      case Auth.TOTP.validate_code(user, code) do
        {:ok, user} ->
          conn
          |> TwoFactor.Session.maybe_set_remember_2fa(user, params["remember_2fa"])
          |> UserAuth.log_in_user(user, params["return_to"])

        {:error, :invalid_code} ->
          Auth.log_failed_login_attempt("wrong 2FA verification code provided for #{user.email}")

          conn
          |> put_flash(:error, "The provided code is invalid. Please try again")
          |> render("verify_2fa.html",
            remember_2fa_days: TwoFactor.Session.remember_2fa_days()
          )

        {:error, :not_enabled} ->
          UserAuth.log_in_user(conn, user, params["return_to"])
      end
    end
  end

  def verify_2fa_recovery_code_form(conn, _params) do
    case TwoFactor.Session.get_2fa_user(conn) do
      {:ok, user} ->
        if Auth.TOTP.enabled?(user) do
          render(conn, "verify_2fa_recovery_code.html")
        else
          redirect_to_login(conn)
        end

      {:error, :not_found} ->
        redirect_to_login(conn)
    end
  end

  def verify_2fa_recovery_code(conn, %{"recovery_code" => recovery_code}) do
    with {:ok, user} <- get_2fa_user_limited(conn) do
      case Auth.TOTP.use_recovery_code(user, recovery_code) do
        :ok ->
          UserAuth.log_in_user(conn, user)

        {:error, :invalid_code} ->
          Auth.log_failed_login_attempt("wrong 2FA recovery code provided for #{user.email}")

          conn
          |> put_flash(:error, "The provided recovery code is invalid. Please try another one")
          |> render("verify_2fa_recovery_code.html")

        {:error, :not_enabled} ->
          UserAuth.log_in_user(conn, user)
      end
    end
  end

  defp get_2fa_user_limited(conn) do
    case TwoFactor.Session.get_2fa_user(conn) do
      {:ok, user} ->
        with :ok <- Auth.rate_limit(:login_ip, conn),
             :ok <- Auth.rate_limit(:login_user, user) do
          {:ok, user}
        else
          {:error, {:rate_limit, _}} ->
            Auth.log_failed_login_attempt("too many login attempts for #{user.email}")

            conn
            |> TwoFactor.Session.clear_2fa_user()
            |> render_error(
              429,
              "Too many login attempts. Wait a minute before trying again."
            )
        end

      {:error, :not_found} ->
        conn
        |> redirect(to: Routes.auth_path(conn, :login_form))
    end
  end

  defp handle_email_updated(conn) do
    conn
    |> put_flash(:success, "Email updated successfully")
    |> redirect(to: Routes.settings_path(conn, :security) <> "#update-email")
  end

  def delete_me(conn, params) do
    case Plausible.Auth.delete_user(conn.assigns[:current_user]) do
      {:ok, :deleted} ->
        logout(conn, params)

      {:error, :active_subscription} ->
        conn
        |> put_flash(
          :error,
          "You have an active subscription which must be canceled first."
        )
        |> redirect(to: Routes.settings_path(conn, :danger_zone))

      {:error, :is_only_team_owner} ->
        conn
        |> put_flash(
          :error,
          "You can't delete your account when you are the only owner on a team."
        )
        |> redirect(to: Routes.settings_path(conn, :danger_zone))
    end
  end

  def logout(conn, params) do
    redirect_to = Map.get(params, "redirect", "/")

    conn
    |> UserAuth.log_out_user()
    |> redirect(to: redirect_to)
  end

  def google_auth_callback(conn, %{"error" => error, "state" => state} = params) do
    [site_id, redirected_to | _] = Jason.decode!(state)

    site = Repo.get(Plausible.Site, site_id)

    redirect_route =
      if redirected_to == "import" do
        Routes.site_path(conn, :settings_imports_exports, site.domain)
      else
        Routes.site_path(conn, :settings_integrations, site.domain)
      end

    case error do
      "access_denied" ->
        conn
        |> put_flash(
          :error,
          "We were unable to authenticate your Google Analytics account. Please check that you have granted us permission to 'See and download your Google Analytics data' and try again."
        )
        |> redirect(to: redirect_route)

      message when message in ["server_error", "temporarily_unavailable"] ->
        conn
        |> put_flash(
          :error,
          "We are unable to authenticate your Google Analytics account because Google's authentication service is temporarily unavailable. Please try again in a few moments."
        )
        |> redirect(to: redirect_route)

      _any ->
        Sentry.capture_message("Google OAuth callback failed. Reason: #{inspect(params)}")

        conn
        |> put_flash(
          :error,
          "We were unable to authenticate your Google Analytics account. If the problem persists, please contact support for assistance."
        )
        |> redirect(to: redirect_route)
    end
  end

  def google_auth_callback(conn, %{"code" => code, "state" => state}) do
    res = Plausible.Google.API.fetch_access_token!(code)

    [site_id, redirect_to | _] = Jason.decode!(state)

    site = Repo.get(Plausible.Site, site_id)
    expires_at = NaiveDateTime.add(NaiveDateTime.utc_now(), res["expires_in"])

    case redirect_to do
      "import" ->
        redirect(conn,
          to:
            Routes.google_analytics_path(conn, :property_form, site.domain,
              access_token: res["access_token"],
              refresh_token: res["refresh_token"],
              expires_at: NaiveDateTime.to_iso8601(expires_at)
            )
        )

      _ ->
        id_token = res["id_token"]
        [_, body, _] = String.split(id_token, ".")
        id = body |> Base.decode64!(padding: false) |> Jason.decode!()

        Plausible.Site.GoogleAuth.changeset(%Plausible.Site.GoogleAuth{}, %{
          email: id["email"],
          refresh_token: res["refresh_token"],
          access_token: res["access_token"],
          expires: expires_at,
          user_id: conn.assigns[:current_user].id,
          site_id: site_id
        })
        |> Repo.insert!()

        site = Repo.get(Plausible.Site, site_id)

        redirect(conn, to: Routes.site_path(conn, :settings_integrations, site.domain))
    end
  end

  defp redirect_to_login(conn) do
    redirect(conn, to: Routes.auth_path(conn, :login_form))
  end
end
