# frozen_string_literal: true

#
# Copyright (C) 2011 - 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/>.
#

class Role < ActiveRecord::Base
  NULL_ROLE_TYPE = "NoPermissions"

  ENROLLMENT_TYPES = %w[StudentEnrollment TeacherEnrollment TaEnrollment DesignerEnrollment ObserverEnrollment].freeze

  DEFAULT_ACCOUNT_TYPE = "AccountMembership"
  ACCOUNT_TYPES = ["AccountAdmin", "AccountMembership"].freeze

  BASE_TYPES = (ACCOUNT_TYPES + ENROLLMENT_TYPES + [NULL_ROLE_TYPE]).freeze
  KNOWN_TYPES = (BASE_TYPES +
    %w[StudentViewEnrollment
       NilEnrollment
       teacher
       ta
       designer
       student
       observer]).freeze

  module AssociationHelper
    # this is an override to take advantage of built-in role caching since those are by far the most common
    def role
      return super if association(:role).loaded?

      self.role = shard.activate do
        # Use `default_canvas_role` even though `default_role` sounds better since default_role is a rails method in rails >= 6.1
        Role.get_role_by_id(role_id) || (respond_to?(:default_canvas_role) ? default_canvas_role : nil)
      end
    end

    def self.included(klass)
      klass.before_save(:resolve_cross_account_role)
    end

    def resolve_cross_account_role
      if will_save_change_to_role_id? && respond_to?(:root_account_id) && root_account_id && role.root_account_id != root_account_id
        self.role = role.role_for_root_account_id(root_account_id)
      end
    end
  end

  belongs_to :account
  belongs_to :root_account, class_name: "Account"
  has_many :role_overrides

  before_validation :infer_root_account_id, if: :belongs_to_account?

  validate :ensure_unique_name_for_account, if: :belongs_to_account?
  validates :name, :workflow_state, presence: true
  validates :account_id, presence: { if: :belongs_to_account? }

  validates :base_role_type, inclusion: { in: BASE_TYPES, message: ->(_object, _data) { t("is invalid") } }
  validates :name, exclusion: { in: KNOWN_TYPES, unless: :built_in?, message: ->(_object, _data) { t("is reserved") } }
  validate :ensure_non_built_in_name

  def role_for_root_account_id(target_root_account_id)
    if built_in? &&
       root_account_id != target_root_account_id &&
       (target_role = Role.get_built_in_role(name, root_account_id: target_root_account_id))
      target_role
    else
      self
    end
  end

  def ensure_unique_name_for_account
    if active?
      scope = Role.where("name = ? AND account_id = ? AND workflow_state = ?", name, account_id, "active")
      if new_record? ? scope.exists? : scope.where.not(id:).exists?
        errors.add(:label, t(:duplicate_role, "A role with this name already exists"))
        false
      end
    end
  end

  def ensure_non_built_in_name
    if !built_in? && Role.built_in_roles(root_account_id:).map(&:label).include?(name)
      errors.add(:label, t(:duplicate_role, "A role with this name already exists"))
      false
    end
  end

  def infer_root_account_id
    unless account
      errors.add(:account_id)
      throw :abort
    end
    self.root_account_id = account.resolved_root_account_id
  end

  include Workflow

  workflow do
    state :active do
      event :deactivate, transitions_to: :inactive
    end
    state :inactive do
      event :activate, transitions_to: :active
    end
    state :built_in # for previously built-in roles
    state :deleted
  end

  def belongs_to_account?
    !built_in? && !deleted?
  end

  def self.built_in_roles(root_account_id:)
    raise "root_account_id required" unless root_account_id

    # giving up on in-process built-in role caching because it's probably not really worth it anymore
    RequestCache.cache("built_in_roles", root_account_id) do
      local_id, shard = Shard.local_id_for(root_account_id)
      (shard || Shard.current).activate do
        Role.where(workflow_state: "built_in", root_account_id: local_id).order(:id).to_a
      end
    end
  end

  def self.built_in_course_roles(root_account_id:)
    built_in_roles(root_account_id:).select(&:course_role?)
  end

  def self.visible_built_in_roles(root_account_id:)
    built_in_roles(root_account_id:).select(&:visible?)
  end

  def self.get_role_by_id(id)
    return nil unless id
    return nil if id.is_a?(String) && id !~ Api::ID_REGEX

    Role.find_by(id:) # giving up on built-in role caching because it's silly now and we should just preload more
  end

  def self.get_built_in_role(name, root_account_id:)
    built_in_roles(root_account_id:).detect { |role| role.name == name }
  end

  def ==(other_role)
    if other_role.is_a?(Role) && built_in? && other_role.built_in?
      name == other_role.name # be equivalent even if they're on different shards/root_accounts
    else
      super
    end
  end

  def visible?
    active? || (built_in? && !["AccountMembership", "NoPermissions"].include?(name))
  end

  def account_role?
    ACCOUNT_TYPES.include?(base_role_type)
  end

  def course_role?
    ENROLLMENT_TYPES.include?(base_role_type)
  end

  def label
    if built_in?
      if course_role?
        RoleOverride.enrollment_type_labels(account || root_account).detect { |label| label[:name] == name }[:label].call
      elsif name == "AccountAdmin"
        RoleOverride::ACCOUNT_ADMIN_LABEL.call
      else
        name
      end
    else
      name
    end
  end

  # Should order course roles so we get "StudentEnrollment", custom student roles, "Teacher Enrollment", custom teacher roles, etc
  # then sort alphabetically within groups
  def display_sort_index
    group_order = if course_role?
                    (ENROLLMENT_TYPES.index(base_role_type) * 2) + (built_in? ? 0 : 1)
                  else
                    built_in? ? 0 : 1
                  end
    [group_order, Canvas::ICU.collation_key(label)]
  end

  alias_method :destroy_permanently!, :destroy
  def destroy
    self.workflow_state = "deleted"
    self.deleted_at = Time.now.utc
    save!
  end

  scope :not_deleted, -> { where("roles.workflow_state IN ('active', 'inactive')") }
  scope :deleted, -> { where(workflow_state: "deleted") }
  scope :active, -> { where(workflow_state: "active") }
  scope :inactive, -> { where(workflow_state: "inactive") }
  scope :for_courses, -> { where(base_role_type: ENROLLMENT_TYPES) }
  scope :for_accounts, -> { where(base_role_type: ACCOUNT_TYPES) }
  scope :full_account_admin, -> { where(base_role_type: "AccountAdmin") }
  scope :custom_account_admin_with_permission, lambda { |permission|
    where(base_role_type: "AccountMembership")
      .where("EXISTS (
      SELECT 1
      FROM #{RoleOverride.quoted_table_name}
      WHERE role_overrides.role_id = roles.id
        AND role_overrides.permission = ?
        AND role_overrides.enabled = ?
    )",
             permission,
             true)
  }

  # Returns a list of hashes for each base enrollment type, and each will have a
  # custom_roles key, each will look like:
  # [{:base_role_name => "StudentEnrollment",
  #   :name => "StudentEnrollment",
  #   :label => "Student",
  #   :plural_label => "Students",
  #   :custom_roles =>
  #           [{:base_role_name => "StudentEnrollment",
  #             :name => "weirdstudent",
  #             :asset_string => "role_4"
  #             :label => "weirdstudent"}]},
  # ]
  def self.all_enrollment_roles_for_account(account, include_inactive = false)
    custom_roles = account.available_custom_course_roles(include_inactive)
    RoleOverride.enrollment_type_labels(account).map do |br|
      new = br.clone
      new[:id] = Role.get_built_in_role(br[:name], root_account_id: account.resolved_root_account_id).id
      new[:label] = br[:label].call
      new[:plural_label] = br[:plural_label].call
      new[:custom_roles] = custom_roles.select { |cr| cr.base_role_type == new[:base_role_name] }.map do |cr|
        { id: cr.id, base_role_name: cr.base_role_type, name: cr.name, label: cr.name, asset_string: cr.asset_string, workflow_state: cr.workflow_state }
      end
      new
    end
  end

  # returns same hash as all_enrollment_roles_for_account but adds enrollment
  # counts for the given course to each item
  def self.custom_roles_and_counts_for_course(course, user, include_inactive = false)
    users_scope = course.users_visible_to(user)
    built_in_role_ids = Role.built_in_course_roles(root_account_id: course.root_account_id).map(&:id)
    base_counts = users_scope.where(enrollments: { role_id: built_in_role_ids })
                             .group("enrollments.type").select("users.id").distinct.count
    role_counts = users_scope.where.not(enrollments: { role_id: built_in_role_ids })
                             .group("enrollments.role_id").select("users.id").distinct.count

    @enrollment_types = Role.all_enrollment_roles_for_account(course.account, include_inactive)
    @enrollment_types.each do |base_type|
      base_type[:count] = base_counts[base_type[:name]] || 0
      base_type[:custom_roles].each do |custom_role|
        id = custom_role[:id]
        custom_role[:count] = role_counts[id] || 0
      end
    end

    @enrollment_types
  end

  def self.add_delete_roles_by_user(user, context)
    is_blueprint = context.is_a?(Course) && MasterCourses::MasterTemplate.is_master_course?(context)
    addable = []
    deleteable = []
    addable += ["TeacherEnrollment"] if context.grants_right?(user, :add_teacher_to_course)
    deleteable += ["TeacherEnrollment"] if context.grants_right?(user, :remove_teacher_from_course)
    addable += ["TaEnrollment"] if context.grants_right?(user, :add_ta_to_course)
    deleteable += ["TaEnrollment"] if context.grants_right?(user, :remove_ta_from_course)
    addable += ["DesignerEnrollment"] if context.grants_right?(user, :add_designer_to_course)
    deleteable += ["DesignerEnrollment"] if context.grants_right?(user, :remove_designer_from_course)
    addable += ["StudentEnrollment"] if context.grants_right?(user, :add_student_to_course) && !is_blueprint
    deleteable += ["StudentEnrollment"] if context.grants_right?(user, :remove_student_from_course)
    addable += ["ObserverEnrollment"] if context.grants_right?(user, :add_observer_to_course) && !is_blueprint
    deleteable += ["ObserverEnrollment"] if context.grants_right?(user, :remove_observer_from_course)

    [addable, deleteable]
  end

  def self.compile_manageable_roles(role_data, user, context)
    # for use with the old sad enrollment dialog
    addable, deleteable = add_delete_roles_by_user(user, context)
    role_data.each_with_object([]) do |role, roles|
      is_addable = addable.include?(role[:base_role_name])
      is_deleteable = deleteable.include?(role[:base_role_name])

      role[:addable_by_user] = is_addable
      role[:deleteable_by_user] = is_deleteable

      custom_roles = role.delete(:custom_roles)
      roles << role

      custom_roles.each do |custom_role|
        custom_role[:addable_by_user] = is_addable
        custom_role[:deleteable_by_user] = is_deleteable
        roles << custom_role
      end
    end
  end

  def self.role_data(course, user, include_inactive = false)
    role_data = custom_roles_and_counts_for_course(course, user, include_inactive)
    compile_manageable_roles(role_data, user, course)
  end

  def self.course_role_data_for_account(account, user)
    role_data = all_enrollment_roles_for_account(account)
    compile_manageable_roles(role_data, user, account)
  end
end
