# frozen_string_literal: true

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

module PermissionsHelper
  def manageable_enrollments_by_permission(permission, enrollments = nil)
    permission = permission.to_sym
    raise "invalid permission" unless RoleOverride.permissions.key?(permission)

    enrollments ||= participating_enrollments
    ActiveRecord::Associations.preload(enrollments, :course)
    ActiveRecord::Associations.preload(enrollments, :enrollment_state)
    allowed_ens = []
    Shard.partition_by_shard(enrollments) do |sharded_enrollments|
      perms_hash = get_permissions_info_by_account(sharded_enrollments.map(&:course), sharded_enrollments, [permission])
      allowed_ens += sharded_enrollments.select do |e|
        perm_hash = perms_hash[e.course.account_id]
        perm_hash && (enabled_for_account_admin(perm_hash, permission) ||
          enabled_for_enrollment(e.role_id, e.type, e.state_based_on_date, perm_hash, permission))
      end
    end
    allowed_ens
  end

  # will return a hash linking global course ids with precalculated permissions
  # e.g. {10000000000001 => {:manage_calendar => true, :manage_grades => false}}
  def precalculate_permissions_for_courses(courses, permissions, loaded_root_accounts = [])
    courses = courses.reject(&:deleted?) # just in case
    permissions = permissions.map(&:to_sym)
    nonexistent_permissions = permissions - RoleOverride.permissions.keys
    raise "invalid permissions - #{nonexistent_permissions}" if nonexistent_permissions.any?

    precalculated_map = {}
    Shard.partition_by_shard(courses, :shard.to_proc) do |sharded_courses|
      unpublished, published = sharded_courses.partition(&:unpublished?)
      all_applicable_enrollments = []
      enrollment_scope = Enrollment.not_inactive_by_date.for_user(self).select("enrollments.*, enrollment_states.state AS date_based_state_in_db")
      if unpublished.any?
        all_applicable_enrollments += enrollment_scope.where(course_id: unpublished)
                                                      .where(type: %w[TeacherEnrollment TaEnrollment DesignerEnrollment StudentViewEnrollment]).to_a
      end
      all_applicable_enrollments += enrollment_scope.where(course_id: published).to_a if published.any?

      grouped_enrollments = all_applicable_enrollments.group_by(&:course_id)
      sharded_courses.each do |course|
        grouped_enrollments[course.id] ||= []
        grouped_enrollments[course.id].each { |e| e.course = course }
      end

      root_account_ids = sharded_courses.map(&:root_account_id).uniq
      unloaded_ra_ids = root_account_ids - loaded_root_accounts.map(&:id)
      root_accounts = loaded_root_accounts + (unloaded_ra_ids.any? ? Account.where(id: unloaded_ra_ids).to_a : [])

      roles = root_accounts.map { |ra| self.roles(ra) }.flatten.uniq
      return nil if roles.include?("consortium_admin") # cross-shard precalculation doesn't work - just fallback to the usual calculations

      is_account_admin = roles.include?("admin")
      account_roles = is_account_admin ? AccountUser.where(user: self).active.preload(:role).to_a : []
      all_permissions_data = get_permissions_info_by_account(sharded_courses, all_applicable_enrollments, permissions, account_roles)

      sharded_courses.each do |course|
        course_permissions = {}
        permissions.each do |permission|
          perm_hash = all_permissions_data[course.account_id]
          course_permissions[permission] = !!(perm_hash &&
            (enabled_for_account_admin(perm_hash, permission) || grouped_enrollments[course.id].any? do |e|
               enabled_for_enrollment(e.role_id, e.type, e.date_based_state_in_db.to_sym, perm_hash, permission)
             end))
        end

        # load some other permissions that we can possibly skip calculating - we can't say for sure they're false but we can mark them true
        active_ens = grouped_enrollments[course.id].select { |e| e.date_based_state_in_db.to_sym == :active }
        course_permissions[:read] = true if active_ens.any?
        if active_ens.any?(&:student?)
          course_permissions[:read_grades] = true
          course_permissions[:participate_as_student] = true
        end
        if grouped_enrollments[course.id].any?(&:admin?)
          course_permissions[:read] = true
          course_permissions[:read_as_admin] = true
        elsif !is_account_admin
          course_permissions[:read_as_admin] = false # wait a second i can totally mark this one as false if they don't have any account users
        end
        precalculated_map[course.global_id] = course_permissions
      end
    end
    precalculated_map
  end

  def enabled_for_account_admin(perm_hash, permission)
    # enabled by account role
    permission_details = RoleOverride.permissions[permission]
    true_for_roles = permission_details[:true_for]
    available_to_roles = permission_details[:available_to]

    perm_hash[:admin_roles].any? do |role|
      if available_to_roles.include?(role.base_role_type)
        role_on = perm_hash.dig(:role_overrides, [role.id, permission], :enabled) && perm_hash.dig(:role_overrides, [role.id, permission], :self)
        role_on.nil? ? true_for_roles.include?(role.base_role_type) : role_on
      end
    end
  end

  def enabled_for_enrollment(role_id, role_type, enrollment_state, perm_hash, permission)
    role_type = "StudentEnrollment" if role_type == "StudentViewEnrollment"
    permission_details = RoleOverride.permissions[permission]
    true_for_roles = permission_details[:true_for]
    available_to_roles = permission_details[:available_to]

    # enabled for enrollment role
    if enrollment_state == :completed
      concluded_roles = permission_details[:applies_to_concluded]
      return false unless concluded_roles
      return false if concluded_roles.is_a?(Array) && !concluded_roles.include?(role_type)
    elsif enrollment_state != :active # future
      return false if permission_details[:restrict_future_enrollments]
    end

    if available_to_roles.include?(role_type)
      role_on = perm_hash.dig(:role_overrides, [role_id, permission], :enabled) && perm_hash.dig(:role_overrides, [role_id, permission], :self)
      role_on.nil? ? true_for_roles.include?(role_type) : role_on
    else
      false
    end
  end

  # examines permissions for accounts related to enrollments and returns a map from account_id to a hash containing
  #  sub_accounts: set of subaccount ids
  #  role_overrides: map from role id to hash containing :enabled, :locked, :self, :children
  #   (these are calculated for the specific account, taking inheritance and locking into account)
  #  admin_roles: set of Roles the user has active account memberships for in this account
  def get_permissions_info_by_account(courses, enrollments, permissions, account_roles = nil)
    account_roles ||= AccountUser.where(user: self).active.preload(:role).to_a
    role_ids = (enrollments.map(&:role_id) + account_roles.map(&:role_id)).uniq
    root_account_ids = courses.map(&:root_account_id).uniq
    query = <<~SQL.squish
      WITH RECURSIVE t(id, name, parent_account_id, role_id, enabled, locked, self, children, permission) AS (
        SELECT accounts.id, name, parent_account_id, ro.role_id, ro.enabled, ro.locked,
          ro.applies_to_self, ro.applies_to_descendants, ro.permission
        FROM #{Account.quoted_table_name}
        LEFT JOIN #{RoleOverride.quoted_table_name} AS ro ON ro.context_id = accounts.id
                                                         AND ro.context_type = 'Account'
                                                         AND ro.permission IN (:permissions)
                                                         AND ro.role_id IN (:role_ids)
        WHERE accounts.id IN (:account_ids)
      UNION
        SELECT accounts.id, accounts.name, accounts.parent_account_id, ro.role_id, ro.enabled,
          ro.locked, ro.applies_to_self, ro.applies_to_descendants, ro.permission
        FROM #{Account.quoted_table_name}
        INNER JOIN t ON accounts.id = t.parent_account_id
        LEFT JOIN #{RoleOverride.quoted_table_name} AS ro ON ro.context_id = accounts.id
                                                         AND ro.context_type = 'Account'
                                                         AND ro.permission IN (:permissions)
                                                         AND ro.role_id IN (:role_ids)
        WHERE accounts.workflow_state = 'active'
      )
      SELECT *
      FROM t
    SQL
    params = {
      account_ids: courses.map(&:account_id),
      permissions:,
      role_ids:
    }
    rows = User.connection.execute(sanitize_sql([query, params]))
    hash_permissions(rows, root_account_ids, account_roles)
  end

  private

  def hash_permissions(rows, root_account_ids, account_roles)
    perms_hash = {}
    new_perm = { sub_accounts: Set.new, role_overrides: {}, admin_roles: Set.new }
    root_account_ids.each { |ri| perms_hash[ri] = new_perm.deep_dup }
    rows.each do |row|
      account_id = row["id"]
      parent_id = row["parent_account_id"]
      role_id = row["role_id"]
      permission = row["permission"]
      perms_hash[account_id] ||= new_perm.deep_dup
      if role_id && permission
        override = { enabled: row["enabled"], locked: row["locked"], self: row["self"], children: row["children"] }
        perms_hash[account_id][:role_overrides][[role_id, permission.to_sym]] = override
      end
      perms_hash[account_id][:admin_roles] += account_roles.select { |au| au.account_id == account_id }.map(&:role)
      if parent_id
        perms_hash[parent_id] ||= new_perm.deep_dup
        perms_hash[parent_id][:sub_accounts] << row["id"]
      end
    end
    root_account_ids.each do |rai|
      fill_permissions_recursive(perms_hash, rai, perms_hash[rai])
    end
    perms_hash
  end

  def fill_permissions_recursive(perms_hash, id, parent_hash)
    perm_hash = perms_hash[id]
    unless parent_hash == perm_hash
      parent_hash[:role_overrides].each_pair do |ro, parent_values|
        next if parent_values[:children] == false && !parent_values[:locked]

        perm_hash[:role_overrides][ro] = parent_values.slice(:locked, :enabled, :children) if perm_hash[:role_overrides][ro].nil? || parent_values[:locked]
        perm_hash[:role_overrides][ro][:self] = parent_values[:children] if perm_hash[:role_overrides][ro][:self].nil?
      end
      perm_hash[:admin_roles] += parent_hash[:admin_roles]
    end
    perm_hash[:sub_accounts].each { |sa| fill_permissions_recursive(perms_hash, sa, perm_hash) }
  end
end
