require 'base64'
require 'securerandom'

require_dependency 'carto/user_authenticator'
require_dependency 'carto/email_cleaner'
require_dependency 'carto/errors'

Rails.configuration.middleware.use RailsWarden::Manager do |manager|
  manager.default_strategies :password, :api_authentication
  manager.failure_app = SessionsController
end

# All strategies should:
# - Include this module
# - Override the methods as needed
module CartoStrategy

  include ::LoggerHelper

  def affected_by_password_expiration?
    true
  end

  def affected_by_reset_password_locked?
    false
  end

  def check_password_expired(user)
    if affected_by_password_expiration? && user.password_expired?
      throw(:warden, action: :password_change, username: user.username)
    end
  end

  def reset_password_rate_limit(user)
    user.reset_password_rate_limit if affected_by_reset_password_locked?
  end

  def trigger_login_event(user)
    check_password_expired(user)
    reset_password_rate_limit(user)
    CartoGearsApi::Events::EventManager.instance.notify(CartoGearsApi::Events::UserLoginEvent.new(user))

    # From the very beginning it's been assumed that after login you go to the dashboard, and
    # we're using that event as a synonymous to "last logged in date". Now you can skip dashboard
    # after login (see #11946), so marking that event on authentication is more accurate with the
    # meaning (although not with the name).
    user.view_dashboard
    begin
      user.update_in_central
    rescue StandardError => e
      log_warning(message: "Error updating lastlogin_date in central", exception: e)
    end
  end
end

# Setup Session Serialization
class Warden::SessionSerializer
  def serialize(user)
    user.username
  end

  def deserialize(username)
    ::User.filter(username: username).first
  end
end

Warden::Strategies.add(:password) do
  include Carto::UserAuthenticator
  include Carto::EmailCleaner
  include CartoStrategy

  def affected_by_reset_password_locked?
    true
  end

  def valid_password_strategy_for_user(user)
    user.organization.nil? || user.organization.auth_username_password_enabled
  end

  def authenticate!
    if params[:email] && params[:password]
      if (user = authenticate(clean_email(params[:email]), params[:password]))
        if user.enabled? && valid_password_strategy_for_user(user)
          trigger_login_event(user)

          success!(user, :message => "Success")
          request.flash['logged'] = true
        elsif !user.enable_account_token.nil?
          throw(:warden, :action => 'account_token_authentication_error', :user_id => user.id)
        else
          fail!
        end
      else
        fail!
      end
    else
      fail!
    end
  end
end

Warden::Strategies.add(:enable_account_token) do
  include CartoStrategy

  def authenticate!
    if params[:id]
      user = ::User.where(enable_account_token: params[:id]).first
      if user
        user.enable_account_token = nil
        user.save

        trigger_login_event(user)

        success!(user)
      else
        fail!
      end
    else
      fail!
    end
  end
end

Warden::Strategies.add(:oauth) do
  include CartoStrategy

  def valid_oauth_strategy_for_user(user)
    user.organization.nil? || user.organization.auth_github_enabled
  end

  def authenticate!
    fail! unless env[:oauth_api]
    oauth_api = env[:oauth_api]
    user = oauth_api.user
    if user && oauth_api.config.valid_method_for?(user)
      trigger_login_event(user)

      success!(user)
    else
      fail!
    end
  end
end

Warden::Strategies.add(:ldap) do
  include CartoStrategy

  def affected_by_password_expiration?
    false
  end

  def authenticate!
    (fail! and return) if (params[:email].blank? || params[:password].blank?)

    user = nil
    begin
      user = Carto::Ldap::Manager.new.authenticate(params[:email], params[:password])
    rescue Carto::Ldap::LDAPUserNotPresentAtCartoDBError => exception
      throw(:warden, action: 'ldap_user_not_at_cartodb',
        cartodb_username: exception.cartodb_username, organization_id: exception.organization_id,
        ldap_username: exception.ldap_username, ldap_email: exception.ldap_email)
    end
    # Fails, but do not stop processin other strategies (allows fallbacks)
    return unless user

    trigger_login_event(user)

    success!(user, :message => "Success")
    request.flash['logged'] = true
  end
end

Warden::Strategies.add(:api_authentication) do
  include CartoStrategy

  def affected_by_password_expiration?
    false
  end

  def authenticate!
    # WARNING: The following code is a modified copy of the oauth10_token method from
    # oauth-plugin-0.4.0.pre4/lib/oauth/controllers/application_controller_methods.rb
    # It also checks token class like does the oauth10_access_token method of that same file
    if Carto::ClientApplication.verify_request(request) do |request_proxy|
          @oauth_token = Carto::ClientApplication.find_token(request_proxy.token)
          if @oauth_token.respond_to?(:provided_oauth_verifier=)
            @oauth_token.provided_oauth_verifier=request_proxy.oauth_verifier
          end
          # return the token secret and the consumer secret
          [(@oauth_token.nil? ? nil : @oauth_token.secret), (@oauth_token.nil? || @oauth_token.client_application.nil? ? nil : @oauth_token.client_application.secret)]
        end

      if @oauth_token && @oauth_token.is_a?(Carto::AccessToken)
        user = ::User.find_with_custom_fields(@oauth_token.user_id)
        if user.enable_account_token.nil?
          success!(user) and return
        else
          throw(:warden, :action => 'account_token_authentication_error', :user_id => user.id)
        end
      end
    end
    fail!
  end
end

Warden::Strategies.add(:http_header_authentication) do
  include CartoStrategy

  def affected_by_password_expiration?
    false
  end

  def valid?
    Carto::HttpHeaderAuthentication.new.valid?(request)
  end

  def authenticate!
    user = Carto::HttpHeaderAuthentication.new.get_user(request)
    return fail! unless user.present?

    trigger_login_event(user)

    success!(user)
  rescue StandardError => e
    CartoDB.report_exception(e, "Authenticating with http_header_authentication", user: user)
    return fail!
  end
end

Warden::Strategies.add(:saml) do
  include CartoStrategy
  include Carto::EmailCleaner
  include ::LoggerHelper

  def affected_by_password_expiration?
    false
  end

  def organization_from_request
    subdomain = CartoDB.extract_subdomain(request)
    Carto::Organization.where(name: subdomain).first if subdomain
  end

  def saml_service(organization = organization_from_request)
    Carto::SamlService.new(organization) if organization
  end

  def valid?
    params[:SAMLResponse].present? && saml_service.try(:enabled?)
  end

  def authenticate!
    organization = organization_from_request
    saml_service = Carto::SamlService.new(organization)

    email = clean_email(saml_service.get_user_email(params[:SAMLResponse]))
    user = organization.users.where(email: email).first

    if user
      if user.try(:enabled?)
        trigger_login_event(user)

        success!(user, message: "Success")
        request.flash['logged'] = true
      else
        fail!
      end
    else
      throw(:warden,
            action: 'saml_user_not_in_carto',
            organization_id: organization.id,
            saml_email: email)
    end
  rescue StandardError => e
    log_error(message: "Authenticating with SAML", exception: e)
    return fail!
  end
end

# @see ApplicationController.update_session_security_token
Warden::Manager.after_set_user except: :fetch do |user, auth, opts|
  auth.session(opts[:scope])[:skip_multifactor_authentication] = auth.winning_strategy && !auth.winning_strategy.store?

  auth.session(opts[:scope])[:sec_token] = user.security_token

  # Only at the editor, and only after new authentications, destroy other sessions
  # @see #4656
  warden_proxy = auth.env['warden']
  # On testing there is no warden global so we cannot run this logic
  if warden_proxy
    warden_sessions = auth.env['rack.session'].to_hash.select do |key, _|
      key.start_with?("warden.user") && !key.end_with?(".session")
    end
    warden_sessions.each do |_, value|
      unless value == user.username
        warden_proxy.logout(value) if warden_proxy.authenticated?(value)
      end
    end
  end
end

Warden::Manager.after_set_user do |user, auth, opts|
  # Without winning strategy (loading cookie from session) assume we want to respect expired passwords
  should_check_expiration = !auth.winning_strategy || auth.winning_strategy.affected_by_password_expiration?
  throw(:warden, action: :password_expired) if should_check_expiration && user.password_expired?
end

Warden::Strategies.add(:user_creation) do
  include CartoStrategy

  def authenticate!
    username = params[:username]
    user = ::User.where(username: username).first
    return fail! unless user

    user_creation = Carto::UserCreation.where(user_id: user.id).first
    return fail! unless user_creation

    if user_creation.autologin?
      trigger_login_event(user)

      success!(user, :message => "Success")
    else
      fail!
    end
  end
end

module Carto::Api::AuthApiAuthentication
  include CartoStrategy
  # We don't want to store a session and send a response cookie
  def store?
    false
  end

  def affected_by_password_expiration?
    false
  end

  def valid?
    base64_auth.present? || params[:api_key].present?
  end

  def base64_auth
    match = AUTH_HEADER_RE.match(request.headers['Authorization'])
    match && match[:auth]
  end

  def authenticate_user(require_master_key)
    user, token = user_and_token_from_request
    return fail! unless user && token

    api_key = user.api_keys.where(token: token)
    api_key = require_master_key ? api_key.master : api_key

    return fail! unless api_key.exists?
    success!(user)
  rescue StandardError
    fail!
  end

  def request_api_key
    return @request_api_key if @request_api_key

    user, token = user_and_token_from_request
    @request_api_key = user.api_keys.where(token: token).first if user && token

    # If user is logged in though other means, assume a master key
    @request_api_key = current_user.api_keys.master.first if !@request_api_key && current_user

    @request_api_key
  end

  private

  AUTH_HEADER_RE = /basic\s(?<auth>\w+)/i

  def user_and_token_from_request
    return unless valid?

    if base64_auth.present?
      username, token = split_auth
      return unless username == CartoDB.extract_subdomain(request)
    elsif params[:api_key]
      token = params[:api_key]
      username = CartoDB.extract_subdomain(request)
    end
    [User[username: username], token]
  end

  def split_auth
    decoded_auth = Base64.decode64(base64_auth)
    decoded_auth.split(':')
  end
end

Warden::Strategies.add(:auth_api) do
  include Carto::Api::AuthApiAuthentication

  def authenticate!
    authenticate_user(true)
  end
end

Warden::Strategies.add(:any_auth_api) do
  include Carto::Api::AuthApiAuthentication

  def authenticate!
    authenticate_user(false)
  end
end
