# frozen_string_literal: true

# Redmine - project management software
# Copyright (C) 2006-2023  Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software

class UserQuery < Query
  self.queried_class = Principal # must be Principal (not User) for custom field filters to work

  self.available_columns = [
    QueryColumn.new(:login, sortable: "#{User.table_name}.login"),
    QueryColumn.new(:firstname, sortable: "#{User.table_name}.firstname"),
    QueryColumn.new(:lastname, sortable: "#{User.table_name}.lastname"),
    QueryColumn.new(:mail, sortable: "#{EmailAddress.table_name}.address"),
    QueryColumn.new(:admin, sortable: "#{User.table_name}.admin"),
    QueryColumn.new(:created_on, :sortable => "#{User.table_name}.created_on"),
    QueryColumn.new(:updated_on, :sortable => "#{User.table_name}.updated_on"),
    QueryColumn.new(:last_login_on, :sortable => "#{User.table_name}.last_login_on"),
    QueryColumn.new(:passwd_changed_on, :sortable => "#{User.table_name}.passwd_changed_on"),
    QueryColumn.new(:status, sortable: "#{User.table_name}.status"),
    QueryAssociationColumn.new(:auth_source, :name, caption: :field_auth_source, sortable: "#{AuthSource.table_name}.name")
  ]

  def initialize(attributes=nil, *args)
    super attributes
    self.filters ||= { 'status' => {operator: "=", values: [User::STATUS_ACTIVE]} }
  end

  def initialize_available_filters
    add_available_filter "status",
      type: :list, values: ->{ user_statuses_values }
    add_available_filter "auth_source_id",
      type: :list_optional, values: ->{ auth_sources_values }
    add_available_filter "is_member_of_group",
      type: :list_optional,
      values: ->{ Group.givable.visible.map {|g| [g.name, g.id.to_s] } }
    if Setting.twofa?
      add_available_filter "twofa_scheme",
        type: :list_optional,
        values: ->{ Redmine::Twofa.available_schemes.map {|s| [I18n.t("twofa__#{s}__name"), s] } }
    end
    add_available_filter "name", type: :text, label: :field_name_or_email_or_login
    add_available_filter "login", type: :string
    add_available_filter "firstname", type: :string
    add_available_filter "lastname", type: :string
    add_available_filter "mail", type: :string
    add_available_filter "created_on", type: :date_past
    add_available_filter "last_login_on", type: :date_past
    add_available_filter "admin",
      type: :list,
      values: [[l(:general_text_yes), '1'], [l(:general_text_no), '0']]
    add_custom_fields_filters(user_custom_fields)
  end

  def auth_sources_values
    AuthSource.order(name: :asc).to_a.map do |auth_source|
      [auth_source.name, auth_source.id]
    end
  end

  def user_statuses_values
    [
      [l(:status_active), User::STATUS_ACTIVE.to_s],
      [l(:status_registered), User::STATUS_REGISTERED.to_s],
      [l(:status_locked), User::STATUS_LOCKED.to_s]
    ]
  end

  def available_columns
    return @available_columns if @available_columns

    @available_columns = self.class.available_columns.dup
    if Setting.twofa?
      @available_columns << QueryColumn.new(:twofa_scheme, sortable: "#{User.table_name}.twofa_scheme")
    end
    @available_columns += user_custom_fields.visible.
                            map {|cf| QueryCustomFieldColumn.new(cf)}

    @available_columns
  end

  # Returns a scope of user custom fields that are available as columns or filters
  def user_custom_fields
    UserCustomField.sorted
  end

  def default_columns_names
    @default_columns_names ||= [:login, :firstname, :lastname, :mail, :admin, :created_on, :last_login_on]
  end

  def default_sort_criteria
    [['login', 'asc']]
  end

  def base_scope
    User.logged.where(statement).includes(:email_address)
  end

  def results_scope(options={})
    order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)

    base_scope.
      order(order_option).
      joins(joins_for_order_statement(order_option.join(',')))
  end

  def sql_for_admin_field(field, operator, value)
    return unless value = value.first

    true_value = operator == '=' ? '1' : '0'
    val =
      if value.to_s == true_value
        self.class.connection.quoted_true
      else
        self.class.connection.quoted_false
      end
    "(#{User.table_name}.admin = #{val})"
  end

  def sql_for_is_member_of_group_field(field, operator, value)
    if ["*", "!*"].include? operator
      value = Group.givable.map(&:id)
    end

    e = operator.start_with?("!") ? "NOT EXISTS" : "EXISTS"

    "(#{e} (SELECT 1 FROM groups_users WHERE #{User.table_name}.id = groups_users.user_id AND #{sql_for_field(field, '=', value, 'groups_users', 'group_id')}))"
  end

  def sql_for_mail_field(field, operator, value)
    if operator == '!*'
      match = false
      operator = '*'
    else
      match = true
    end
    emails = EmailAddress.table_name
    <<-SQL
      #{match ? 'EXISTS' : 'NOT EXISTS'}
      (SELECT 1 FROM #{emails} WHERE
        #{emails}.user_id = #{User.table_name}.id AND
        #{sql_for_field(:mail, operator, value, emails, 'address')})
    SQL
  end

  def joins_for_order_statement(order_options)
    joins = [super]

    if order_options
      if order_options.include?('auth_source')
        joins << "LEFT OUTER JOIN #{AuthSource.table_name} auth_sources ON auth_sources.id = #{queried_table_name}.auth_source_id"
      end
    end

    joins.any? ? joins.join(' ') : nil
  end

  def sql_for_name_field(field, operator, value)
    case operator
    when '*'
      '1=1'
    when '!*'
      '1=0'
    else
      # match = (operator == '~')
      match = !operator.start_with?('!')
      matching_operator = operator.sub /^!/, ''
      name_sql = %w(login firstname lastname).map{|field| sql_for_field(:name, operator, value, User.table_name, field)}

      emails = EmailAddress.table_name
      email_sql = <<-SQL
        #{match ? "EXISTS" : "NOT EXISTS"}
        (SELECT 1 FROM #{emails} WHERE
          #{emails}.user_id = #{User.table_name}.id AND
          #{sql_for_field(:name, matching_operator, value, emails, 'address')})
      SQL

      conditions = name_sql + [email_sql]
      op = match ? " OR " : " AND "
      "(#{conditions.map{|s| "(#{s})"}.join(op)})"
    end
  end
end
