# frozen_string_literal: true

require "csv"

class InvitesController < ApplicationController
  requires_login only: %i[
                   create
                   retrieve
                   destroy
                   destroy_all_expired
                   resend_invite
                   resend_all_invites
                   upload_csv
                 ]

  skip_before_action :check_xhr, except: [:perform_accept_invitation]
  skip_before_action :preload_json, except: [:show]
  skip_before_action :redirect_to_login_if_required
  skip_before_action :redirect_to_profile_if_required

  before_action :ensure_invites_allowed, only: %i[show perform_accept_invitation]
  before_action :ensure_new_registrations_allowed, only: %i[show perform_accept_invitation]

  def show
    expires_now

    RateLimiter.new(nil, "invites-show-#{request.remote_ip}", 100, 1.minute).performed!

    invite = Invite.find_by(invite_key: params[:id])

    if invite.present? && invite.redeemable?
      show_invite(invite)
    else
      show_irredeemable_invite(invite)
    end
  rescue RateLimiter::LimitExceeded => e
    flash.now[:error] = e.description
    render layout: "no_ember"
  end

  def create_multiple
    guardian.ensure_can_bulk_invite_to_forum!(current_user)
    emails = params[:email]
    # validate that topics and groups can accept invites.
    if params[:topic_id].present?
      topic = Topic.find_by(id: params[:topic_id])
      raise Discourse::InvalidParameters.new(:topic_id) if topic.blank?
      guardian.ensure_can_invite_to!(topic)
    end

    if params[:group_ids].present? || params[:group_names].present?
      groups = Group.lookup_groups(group_ids: params[:group_ids], group_names: params[:group_names])
    end

    guardian.ensure_can_invite_to_forum!(groups)

    if !groups_can_see_topic?(groups, topic)
      editable_topic_groups = topic.category.groups.filter { |g| guardian.can_edit_group?(g) }
      return(
        render_json_error(
          I18n.t("invite.requires_groups", groups: editable_topic_groups.pluck(:name).join(", ")),
        )
      )
    end

    if emails.size > SiteSetting.max_api_invites
      return(
        render_json_error(
          I18n.t("invite.max_invite_emails_limit_exceeded", max: SiteSetting.max_api_invites),
          422,
        )
      )
    end

    success = []
    fail = []

    emails.map do |email|
      begin
        invite =
          Invite.generate(
            current_user,
            email: email,
            domain: params[:domain],
            skip_email: params[:skip_email],
            invited_by: current_user,
            custom_message: params["custom_message"],
            max_redemptions_allowed: params[:max_redemptions_allowed],
            topic_id: topic&.id,
            group_ids: groups&.map(&:id),
            expires_at: params[:expires_at],
            invite_to_topic: params[:invite_to_topic],
          )
        success.push({ email: email, invite: invite }) if invite
      rescue Invite::UserExists => e
        fail.push({ email: email, error: e.message })
      rescue ActiveRecord::RecordInvalid => e
        fail.push({ email: email, error: e.record.errors.full_messages.first })
      end
    end

    render json: {
             num_successfully_created_invitations: success.length,
             num_failed_invitations: fail.length,
             failed_invitations: fail,
             successful_invitations:
               success.map do |s| InviteSerializer.new(s[:invite], scope: guardian) end,
           }
  end

  def create
    begin
      if params[:topic_id].present?
        topic = Topic.find_by(id: params[:topic_id])
        raise Discourse::InvalidParameters.new(:topic_id) if topic.blank?
        guardian.ensure_can_invite_to!(topic)
      end

      if params[:group_ids].present? || params[:group_names].present?
        groups =
          Group.lookup_groups(group_ids: params[:group_ids], group_names: params[:group_names])
      end

      guardian.ensure_can_invite_to_forum!(groups)

      if !groups_can_see_topic?(groups, topic)
        editable_topic_groups = topic.category.groups.filter { |g| guardian.can_edit_group?(g) }
        return(
          render_json_error(
            I18n.t("invite.requires_groups", groups: editable_topic_groups.pluck(:name).join(", ")),
          )
        )
      end

      invite =
        Invite.generate(
          current_user,
          email: params[:email],
          domain: params[:domain],
          skip_email: params[:skip_email],
          invited_by: current_user,
          custom_message: params[:custom_message],
          max_redemptions_allowed: params[:max_redemptions_allowed],
          topic_id: topic&.id,
          group_ids: groups&.map(&:id),
          expires_at: params[:expires_at],
          invite_to_topic: params[:invite_to_topic],
        )

      if invite.present?
        render_serialized(
          invite,
          InviteSerializer,
          scope: guardian,
          root: nil,
          show_emails: params.has_key?(:email),
          show_warnings: true,
        )
      else
        render json: failed_json, status: 422
      end
    rescue Invite::UserExists => e
      render_json_error(e.message)
    rescue ActiveRecord::RecordInvalid => e
      render_json_error(e.record.errors.full_messages.first)
    end
  end

  def retrieve
    params.require(:email)

    invite = Invite.find_by(invited_by: current_user, email: params[:email])
    raise Discourse::InvalidParameters.new(:email) if invite.blank?

    guardian.ensure_can_invite_to_forum!(nil)

    render_serialized(
      invite,
      InviteSerializer,
      scope: guardian,
      root: nil,
      show_emails: params.has_key?(:email),
      show_warnings: true,
    )
  end

  def update
    invite = Invite.find_by(invited_by: current_user, id: params[:id])
    raise Discourse::InvalidParameters.new(:id) if invite.blank?

    if params[:topic_id].present?
      topic = Topic.find_by(id: params[:topic_id])
      raise Discourse::InvalidParameters.new(:topic_id) if topic.blank?
      guardian.ensure_can_invite_to!(topic)
    end

    if params[:group_ids].present? || params[:group_names].present?
      groups = Group.lookup_groups(group_ids: params[:group_ids], group_names: params[:group_names])
    end

    guardian.ensure_can_invite_to_forum!(groups)

    Invite.transaction do
      if params.has_key?(:topic_id)
        invite.topic_invites.destroy_all
        invite.topic_invites.create!(topic_id: topic.id) if topic.present?
      end

      if params.has_key?(:group_ids) || params.has_key?(:group_names)
        invite.invited_groups.destroy_all
        if groups.present?
          groups.each { |group| invite.invited_groups.find_or_create_by!(group_id: group.id) }
        end
      end

      if !groups_can_see_topic?(invite.groups, invite.topics.first)
        editable_topic_groups =
          invite.topics.first.category.groups.filter { |g| guardian.can_edit_group?(g) }
        return(
          render_json_error(
            I18n.t("invite.requires_groups", groups: editable_topic_groups.pluck(:name).join(", ")),
          )
        )
      end

      if params.has_key?(:email)
        old_email = invite.email.presence
        new_email = params[:email].presence

        if new_email
          if Invite
               .where.not(id: invite.id)
               .find_by(email: new_email.downcase, invited_by_id: current_user.id)
               &.redeemable?
            return(
              render_json_error(
                I18n.t("invite.invite_exists", email: CGI.escapeHTML(new_email)),
                status: 409,
              )
            )
          end
        end

        if old_email != new_email
          invite.emailed_status =
            if new_email && !params[:skip_email]
              Invite.emailed_status_types[:pending]
            else
              Invite.emailed_status_types[:not_required]
            end
        end

        invite.domain = nil if invite.email.present?
      end

      if params.has_key?(:domain)
        invite.domain = params[:domain]

        if invite.domain.present?
          invite.email = nil
          invite.emailed_status = Invite.emailed_status_types[:not_required]
        end
      end

      if params[:send_email]
        if invite.emailed_status != Invite.emailed_status_types[:pending]
          begin
            RateLimiter.new(current_user, "resend-invite-per-hour", 10, 1.hour).performed!
          rescue RateLimiter::LimitExceeded
            return render_json_error(I18n.t("rate_limiter.slow_down"))
          end
        end

        invite.emailed_status = Invite.emailed_status_types[:pending]
      end

      begin
        invite.update!(
          params.permit(:email, :custom_message, :max_redemptions_allowed, :expires_at),
        )
      rescue ActiveRecord::RecordInvalid => e
        return render_json_error(e.record.errors.full_messages.first)
      end
    end

    if invite.emailed_status == Invite.emailed_status_types[:pending]
      invite.update_column(:emailed_status, Invite.emailed_status_types[:sending])
      Jobs.enqueue(:invite_email, invite_id: invite.id, invite_to_topic: params[:invite_to_topic])
    end

    render_serialized(
      invite,
      InviteSerializer,
      scope: guardian,
      root: nil,
      show_emails: params.has_key?(:email),
      show_warnings: true,
    )
  end

  def destroy
    params.require(:id)

    invite = Invite.find_by(invited_by_id: current_user.id, id: params[:id])
    raise Discourse::InvalidParameters.new(:id) if invite.blank?

    invite.trash!(current_user)

    render json: success_json
  end

  # For DiscourseConnect SSO, all invite acceptance is done
  # via the SessionController#sso_login route
  def perform_accept_invitation
    params.require(:id)
    params.permit(
      :email,
      :username,
      :name,
      :password,
      :timezone,
      :email_token,
      user_custom_fields: {
      },
    )

    invite = Invite.find_by(invite_key: params[:id])
    redeeming_user = current_user

    if invite.present?
      begin
        attrs = { ip_address: request.remote_ip, session: session }

        if redeeming_user
          attrs[:redeeming_user] = redeeming_user
        else
          attrs[:username] = params[:username]
          attrs[:name] = params[:name]
          attrs[:password] = params[:password]
          attrs[:user_custom_fields] = params[:user_custom_fields]

          # If the invite is not scoped to an email then we allow the
          # user to provide it themselves
          if invite.is_invite_link?
            params.require(:email)
            attrs[:email] = params[:email]
          else
            # Otherwise we always use the email from the invitation.
            attrs[:email] = invite.email
            attrs[:email_token] = params[:email_token] if params[:email_token].present?
          end
        end

        user = invite.redeem(**attrs)
      rescue ActiveRecord::RecordInvalid,
             ActiveRecord::RecordNotSaved,
             ActiveRecord::LockWaitTimeout,
             Invite::UserExists => e
        return render json: failed_json.merge(message: e.message), status: 412
      end

      if user.blank?
        return render json: failed_json.merge(message: I18n.t("invite.not_found_json")), status: 404
      end

      log_on_user(user) if !redeeming_user && user.active? && user.guardian.can_access_forum?

      user.update_timezone_if_missing(params[:timezone])
      post_process_invite(user)
      create_topic_invite_notifications(invite, user)

      topic = invite.topics.first
      response = {}

      if user.present?
        if user.active? && user.guardian.can_access_forum?
          response[:message] = I18n.t("invite.existing_user_success") if redeeming_user

          if user.guardian.can_see?(topic)
            response[:redirect_to] = path(topic.relative_url)
          else
            response[:redirect_to] = path("/")
          end
        else
          response[:message] = if user.active?
            I18n.t("activation.approval_required")
          else
            I18n.t("invite.confirm_email")
          end

          cookies[:destination_url] = path(topic.relative_url) if user.guardian.can_see?(topic)
        end
      end

      render json: success_json.merge(response)
    else
      render json: failed_json.merge(message: I18n.t("invite.not_found_json")), status: 404
    end
  end

  def destroy_all_expired
    guardian.ensure_can_destroy_all_invites!(current_user)

    Invite
      .where(invited_by: current_user)
      .where("expires_at < ?", Time.zone.now)
      .find_each { |invite| invite.trash!(current_user) }

    render json: success_json
  end

  def resend_invite
    params.require(:email)
    RateLimiter.new(current_user, "resend-invite-per-hour", 10, 1.hour).performed!

    invite = Invite.find_by(invited_by_id: current_user.id, email: params[:email])
    raise Discourse::InvalidParameters.new(:email) if invite.blank?
    invite.resend_invite
    render json: success_json
  rescue RateLimiter::LimitExceeded
    render_json_error(I18n.t("rate_limiter.slow_down"))
  end

  def resend_all_invites
    guardian.ensure_can_resend_all_invites!(current_user)

    begin
      RateLimiter.new(
        current_user,
        "bulk-reinvite-per-day",
        1,
        1.day,
        apply_limit_to_staff: true,
      ).performed!
    rescue RateLimiter::LimitExceeded
      return render_json_error(I18n.t("rate_limiter.slow_down"))
    end

    Invite
      .pending(current_user)
      .where("invites.email IS NOT NULL")
      .find_each { |invite| invite.resend_invite }

    render json: success_json
  end

  def upload_csv
    guardian.ensure_can_bulk_invite_to_forum!(current_user)

    hijack do
      begin
        file = params[:file] || params[:files].first

        csv_header = nil
        invites = []

        CSV.foreach(file.tempfile, encoding: "bom|utf-8") do |row|
          # Try to extract a CSV header, if it exists
          if csv_header.nil?
            if row[0] == "email"
              csv_header = row
              next
            else
              csv_header = %w[email groups topic_id]
            end
          end

          invites.push(csv_header.zip(row).map.to_h.filter { |k, v| v.present? }) if row[0].present?

          break if invites.count >= SiteSetting.max_bulk_invites
        end

        if invites.present?
          Jobs.enqueue(:bulk_invite, invites: invites, current_user_id: current_user.id)

          if invites.count >= SiteSetting.max_bulk_invites
            render json:
                     failed_json.merge(
                       errors: [
                         I18n.t(
                           "bulk_invite.max_rows",
                           max_bulk_invites: SiteSetting.max_bulk_invites,
                         ),
                       ],
                     ),
                   status: 422
          else
            render json: success_json
          end
        else
          render json: failed_json.merge(errors: [I18n.t("bulk_invite.error")]), status: 422
        end
      end
    end
  end

  private

  def show_invite(invite)
    email = Email.obfuscate(invite.email)

    # Show email if the user already authenticated their email
    different_external_email = false

    if session[:authentication]
      auth_result = Auth::Result.from_session_data(session[:authentication], user: nil)
      if invite.email == auth_result.email
        email = invite.email
      else
        different_external_email = true
      end
    end

    email_verified_by_link = invite.email_token.present? && params[:t] == invite.email_token

    email = invite.email if email_verified_by_link

    hidden_email = email != invite.email

    if hidden_email || invite.email.nil?
      username = ""
    else
      username = UserNameSuggester.suggest(invite.email)
    end

    info = {
      invited_by: UserNameSerializer.new(invite.invited_by, scope: guardian, root: false),
      email: email,
      hidden_email: hidden_email,
      username: username,
      is_invite_link: invite.is_invite_link?,
      email_verified_by_link: email_verified_by_link,
    }

    info[:different_external_email] = true if different_external_email

    if staged_user = User.where(staged: true).with_email(invite.email).first
      info[:username] = staged_user.username
      info[:user_fields] = staged_user.user_fields
    end

    if current_user
      info[:existing_user_id] = current_user.id
      info[:existing_user_can_redeem] = invite.can_be_redeemed_by?(current_user)
      info[:existing_user_can_redeem_error] = existing_user_can_redeem_error(invite)
      info[:email] = current_user.email
      info[:username] = current_user.username
    end

    secure_session["invite-key"] = invite.invite_key

    respond_to do |format|
      format.html { store_preloaded("invite_info", MultiJson.dump(info)) }
      format.json { render_json_dump(info) }
    end
  end

  def show_irredeemable_invite(invite)
    flash.now[:error] = if invite.blank?
      I18n.t("invite.not_found", base_url: Discourse.base_url)
    elsif invite.redeemed?
      if invite.is_invite_link?
        I18n.t(
          "invite.not_found_template_link",
          site_name: SiteSetting.title,
          base_url: Discourse.base_url,
        )
      else
        I18n.t(
          "invite.not_found_template",
          site_name: SiteSetting.title,
          base_url: Discourse.base_url,
        )
      end
    elsif invite.expired?
      I18n.t("invite.expired", base_url: Discourse.base_url)
    end

    render layout: "no_ember"
  end

  def ensure_invites_allowed
    if (
         !SiteSetting.enable_local_logins && Discourse.enabled_auth_providers.count == 0 &&
           !SiteSetting.enable_discourse_connect
       )
      raise Discourse::NotFound
    end
  end

  def ensure_new_registrations_allowed
    unless SiteSetting.allow_new_registrations
      flash[:error] = I18n.t("login.new_registrations_disabled")
      render layout: "no_ember"
      false
    end
  end

  def groups_can_see_topic?(groups, topic)
    if topic&.read_restricted_category?
      topic_groups = topic.category.groups
      return false if (groups & topic_groups).blank?
    end

    true
  end

  def post_process_invite(user)
    user.enqueue_welcome_message("welcome_invite") if user.send_welcome_message

    Group.refresh_automatic_groups!(:admins, :moderators, :staff) if user.staff?

    if user.has_password?
      if !user.active
        email_token =
          user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup])
        EmailToken.enqueue_signup_email(email_token)
      end
    elsif !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins
      Jobs.enqueue(:invite_password_instructions_email, username: user.username)
    end
  end

  def create_topic_invite_notifications(invite, user)
    invite.topics.each do |topic|
      if user.guardian.can_see?(topic)
        last_notification =
          user
            .notifications
            .where(notification_type: Notification.types[:invited_to_topic])
            .where(topic_id: topic.id)
            .where(post_number: 1)
            .where("created_at > ?", 1.hour.ago)

        if !last_notification.exists?
          topic.create_invite_notification!(
            user,
            Notification.types[:invited_to_topic],
            invite.invited_by,
          )
        end
      end
    end
  end

  def existing_user_can_redeem_error(invite)
    return if invite.can_be_redeemed_by?(current_user)
    if invite.invited_users.exists?(user: current_user)
      I18n.t("invite.existing_user_already_redemeed")
    else
      I18n.t("invite.existing_user_cannot_redeem")
    end
  end
end