# frozen_string_literal: true

#
# Copyright (C) 2015 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

require "rqrcode"
require "rotp"

class Login::OtpController < ApplicationController
  include Login::Shared
  include Login::OtpHelper

  before_action :require_user
  before_action :require_password_session
  before_action :forbid_on_files_domain

  def new
    # if we waiting on OTP for login, but we're not yet configured, start configuring
    # OR if we're not waiting on OTP, we're configuring
    if (session[:pending_otp] && !secret_key) ||
       (!session[:pending_otp] && !configuring?)
      session[:pending_otp_secret_key] = ROTP::Base32.random
      @first_reconfiguration = true
    end

    pending_otp_cc_id = session[:pending_otp_communication_channel_id]

    if pending_otp_cc_id
      @cc = @current_user.communication_channels.find(pending_otp_cc_id)
    elsif !configuring?
      @cc = @current_user.otp_communication_channel
    end

    send_otp unless configuring?

    respond_to do |format|
      format.html { add_meta_tag(name: "viewport", id: "vp", content: "initial-scale=1.0,user-scalable=yes,width=device-width") }

      format.json do
        json_response = {
          # OTP setup required
          otp_configuring: configuring?,
          # OTP sent and awaiting verification
          otp_sent: configuring? ? nil : session[:pending_otp].presence,
          # nil if OTP generated by authentication app
          pending_otp_communication_channel_id: pending_otp_cc_id
        }.compact_blank

        render json: json_response, status: :ok
      end
    end
  end

  def send_via_sms
    return render status: :bad_request, text: "can't change destination until you're logged in" unless configuring?

    if params[:otp_login].try(:[], :otp_communication_channel_id)
      cc = @current_user.communication_channels.sms.unretired.find(params[:otp_login][:otp_communication_channel_id])
      session[:pending_otp_communication_channel_id] = cc.id
    end
    if session[:pending_otp_secret_key] && params[:otp_login].try(:[], :phone_number)
      path = params[:otp_login][:phone_number].gsub(/[^\d]/, "").to_s
      cc = @current_user.communication_channels.sms.by_path(path).first
      cc ||= @current_user.communication_channels.sms.create!(path:)
      if cc.retired?
        cc.workflow_state = "unconfirmed"
        cc.save!
      end
      session[:pending_otp_communication_channel_id] = cc.id
    end
    send_otp(cc)

    redirect_to otp_login_url
  end

  def create
    # this action can be called to try to find a valid OTP by chance.  To prevent
    # abuse, we'll make this look like an expensive operation so it would
    # get quickly rate limited if hit repeatedly.  This should be about 1/4
    # of a maxed out bucket.
    increment_request_cost(150)

    verification_code = params[:otp_login][:verification_code].delete(" ")
    if Canvas.redis_enabled?
      key = "otp_used:#{@current_user.global_id}:#{verification_code}"
      if Canvas.redis.get(key)
        force_fail = true
      else
        Canvas.redis.setex(key, 10.minutes, "1")
      end
    end

    drift = 30
    # give them 5 minutes to enter an OTP sent via SMS
    drift = 300 if session[:pending_otp_communication_channel_id] ||
                   (!session[:pending_otp_secret_key] && @current_user.otp_communication_channel_id)

    if (!force_fail && ROTP::TOTP.new(secret_key).verify(verification_code, drift_behind: drift, drift_ahead: drift)) ||
       @current_user.authenticate_one_time_password(verification_code)
      if configuring?
        @current_user.one_time_passwords.scope.delete_all
        @current_user.otp_secret_key = session.delete(:pending_otp_secret_key)
        @current_user.otp_communication_channel_id = session.delete(:pending_otp_communication_channel_id)
        @current_user.otp_communication_channel.try(:confirm)
        @current_user.save!
      end

      if params[:otp_login][:remember_me] == "1"
        now = Time.now.utc
        old_cookie = cookies["canvas_otp_remember_me"]
        old_cookie = nil unless @current_user.validate_otp_secret_key_remember_me_cookie(old_cookie)
        cookies["canvas_otp_remember_me"] = {
          value: @current_user.otp_secret_key_remember_me_cookie(now, old_cookie, request.remote_ip),
          expires: now + 30.days,
          domain: remember_me_cookie_domain,
          httponly: true,
          same_site: :none,
          secure: CanvasRails::Application.config.session_options[:secure],
          path: "/login"
        }
      end
      if session.delete(:pending_otp)
        successful_login(@current_user, @current_pseudonym, true)
      else
        respond_to do |format|
          format.html do
            flash[:notice] = t "Multi-factor authentication configured"
            redirect_to settings_profile_url
          end
          format.json { render json: { otp_configured: true }, status: :ok }
        end
      end
    else
      respond_to do |format|
        format.html do
          flash[:error] = t "Invalid verification code, please try again"
          redirect_to otp_login_url
        end
        format.json { render json: { error: t("Invalid verification code, please try again") }, status: :unprocessable_entity }
      end
    end
  end

  def cancel_otp
    clear_pending_otp
    render json: { message: "Multi-factor authentication process has been cancelled." }, status: :ok
  end

  def destroy
    user = if params[:user_id] == "self"
             @current_user
           else
             User.find(params[:user_id])
           end
    return unless authorized_action(user, @current_user, :reset_mfa)

    user.otp_secret_key = nil
    user.otp_communication_channel = nil
    user.save!
    user.one_time_passwords.scope.delete_all

    render json: {}
  end

  protected

  def send_otp(cc = nil)
    cc ||= @current_user.otp_communication_channel
    key = ROTP::TOTP.new(secret_key).now
    cc&.send_otp!(key, @domain_root_account)
    if cc&.otp_impaired?
      @current_user.email_channel&.send_otp!(key, @domain_root_account)
    end
  end

  def secret_key
    session[:pending_otp_secret_key] || @current_user.otp_secret_key
  end

  def clear_pending_otp
    session.delete(:pending_otp)
    session.delete(:pending_otp_secret_key)
    session.delete(:pending_otp_communication_channel_id)
  end

  def increment_statsd(...)
    # ignore
  end
end
