# frozen_string_literal: true

#
# Copyright (C) 2012 - 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 Api::V1::AssignmentOverride
  include Api::V1::Json

  def assignment_override_json(override, visible_users=nil)
    fields = [:id, :assignment_id, :title]
    fields.concat([:due_at, :all_day, :all_day_date]) if override.due_at_overridden
    fields << :unlock_at if override.unlock_at_overridden
    fields << :lock_at if override.lock_at_overridden
    api_json(override, @current_user, session, :only => fields).tap do |json|
      case override.set_type
      when 'ADHOC'
        if override.preloaded_student_ids
          json[:student_ids] = override.preloaded_student_ids
        else
          json[:student_ids] = if visible_users.present?
                       override.assignment_override_students.where(user_id: visible_users).pluck(:user_id)
                     else
                       override.assignment_override_students.pluck(:user_id)
                     end
        end
      when 'Group'
        json[:group_id] = override.set_id
      when 'CourseSection'
        json[:course_section_id] = override.set_id
      when 'Noop'
        json[:noop_id] = override.set_id
      end
    end
  end

  def assignment_overrides_json(overrides, user = nil)
    visible_users_ids = ::AssignmentOverride.visible_enrollments_for(overrides.compact, user).select(:user_id)
    # we most likely already have the student_ids preloaded here because of overridden_for, but just in case
    if overrides.any?{|ov| ov.present? && ov.set_type == 'ADHOC' && !ov.preloaded_student_ids}
      AssignmentOverrideApplicator.preload_student_ids_for_adhoc_overrides(overrides.select{|ov| ov.set_type == 'ADHOC'}, visible_users_ids)
    end
    overrides.map{ |override| assignment_override_json(override, visible_users_ids) if override }
  end

  def assignment_override_collection(assignment, include_students=false)
    overrides = AssignmentOverrideApplicator.overrides_for_assignment_and_user(assignment, @current_user)
    if include_students
      ActiveRecord::Associations::Preloader.new.preload(overrides, :assignment_override_students)
    end
    overrides
  end

  def find_assignment_override(assignment, set_or_id)
    find_assignment_overrides(assignment, [set_or_id])[0]
  end

  def find_assignment_overrides(assignment, sets_or_ids)
    overrides = assignment_override_collection(assignment)
    sets_or_ids.map do |set_or_id|
      filter_assignment_overrides(overrides, set_or_id)
    end
  end

  def filter_assignment_overrides(overrides, set_or_id)
    return nil if overrides.empty? || set_or_id.nil?
    case set_or_id
    when CourseSection, Group
      overrides.detect do |o|
        o.set_type == set_or_id.class.to_s &&
        o.set_id == set_or_id.id # maybe FIXME
      end
    else
      overrides.detect { |o| o.id == set_or_id.to_i }
    end
  end

  def find_group(assignment, group_id, group_category_id=nil)
    scope = Group.active.where(:context_type => 'Course').where("group_category_id IS NOT NULL")
    if assignment
      scope = scope.where(
        context_id: assignment.context_id,
        group_category_id: (group_category_id || assignment.group_category_id)
      )
    end
    group = scope.find(group_id)
    raise ActiveRecord::RecordNotFound unless group.grants_right?(@current_user, session, :read)
    group
  end

  def find_section(context, section_id)
    scope = CourseSection.active
    scope = scope.where(:course_id => context) if context
    section = api_find(scope, section_id)
    raise ActiveRecord::RecordNotFound unless section.grants_right?(@current_user, session, :read)
    section
  end

  def interpret_assignment_override_data(assignment, data, set_type=nil)
    data ||= {}
    return {}, ["invalid override data"] unless data.is_a?(Hash) || data.is_a?(ActionController::Parameters)

    # validate structure of parameters
    override_data = {}
    errors = []

    if !set_type && data[:student_ids]
      set_type = 'ADHOC'
    end

    if set_type == 'ADHOC' && data[:student_ids]
      # require the ids to be a list
      student_ids = data[:student_ids]
      if !student_ids.is_a?(Array) || student_ids.empty?
        errors << "invalid student_ids #{student_ids.inspect}"
        students = []
      else
        # look up all students since the assignment will affect all current and
        # previous students in the course on this override and not just what the
        # teacher can see that were sent in the request object
        students = api_find_all(assignment.context.all_students, student_ids)
        students = students.distinct if students.is_a?(ActiveRecord::Relation)
        students = students.uniq if students.is_a?(Array)

        # make sure they were all valid
        found_ids = students.map{ |s| [
          s.id.to_s,
          s.global_id.to_s,
          ("sis_login_id:#{s.pseudonym.unique_id}" if s.pseudonym),
          ("hex:sis_login_id:#{s.pseudonym.unique_id.to_s.unpack('H*')}" if s.pseudonym),
          ("sis_user_id:#{s.pseudonym.sis_user_id}" if s.pseudonym && s.pseudonym.sis_user_id),
          ("hex:sis_user_id:#{s.pseudonym.sis_user_id.to_s.unpack('H*')}" if s.pseudonym && s.pseudonym.sis_user_id)
        ]}.flatten.compact
        bad_ids = student_ids.map(&:to_s) - found_ids
        errors << "unknown student ids: #{bad_ids.inspect}" unless bad_ids.empty?
      end
      override_data[:students] = students
    end

    if !set_type && data.key?(:group_id)
      group_category_id = assignment.group_category_id || assignment.discussion_topic.try(:group_category_id)
      if !group_category_id
        # don't recognize group_id for non-group assignments
        errors << "group_id is not valid for non-group assignments"
      else
        set_type = 'Group'
        # look up the group
        begin
          group = find_group(assignment, data[:group_id], group_category_id)
        rescue ActiveRecord::RecordNotFound
          errors << "unknown group id #{data[:group_id].inspect}"
        end
        override_data[:group] = group
      end
    end

    if !set_type && data.key?(:course_section_id)
      set_type = 'CourseSection'

      # look up the section
      begin
        section = find_section(assignment.context, data[:course_section_id])
      rescue ActiveRecord::RecordNotFound
        errors << "unknown section id #{data[:course_section_id].inspect}"
      end
      override_data[:section] = section
    end

    if !set_type && data.key?(:noop_id)
      set_type = 'Noop'
      override_data[:noop_id] = data[:noop_id]
    end

    errors << "one of student_ids, group_id, or course_section_id is required" if !set_type && errors.empty?

    if %w(ADHOC Noop).include?(set_type) && data.key?(:title)
      override_data[:title] = data[:title]
    end

    # collect override values
    [:due_at, :unlock_at, :lock_at].each do |field|
      next unless data.key?(field)

      begin
        if data[field].blank?
          # override value of nil/'' is meaningful
          override_data[field] = nil
        elsif value = Time.zone.parse(data[field].to_s)
          override_data[field] = value
        else
          errors << "invalid #{field} #{data[field].inspect}"
        end
      rescue
        errors << "invalid #{field} #{data[field].inspect}"
      end
    end

    errors = nil if errors.empty?
    return override_data, errors
  end

  def check_property(object, prop, present, errors, message)
    object.each_with_index.reject do |element, i|
      if element[prop].present? != present
        errors[i] ||= []
        errors[i] << message
      end
    end
  end

  # receives data of shape [{ id: 2, assignment_id: 1, ...update_params }, ... ]
  # responds with data of shape [{ assignment: model, override: model, ...update_params }, ...]
  def interpret_batch_assignment_overrides_data(course, assignment_overrides_data, for_update)
    return nil, ['no assignment override data present'] unless assignment_overrides_data.present?
    return nil, ['must specify an array of overrides'] unless assignment_overrides_data.is_a? Array

    all_errors = Array.new(assignment_overrides_data.length)

    check_property(assignment_overrides_data, 'assignment_id', true, all_errors, 'must specify an assignment id')
    if for_update
      check_property(assignment_overrides_data, 'id', true, all_errors, 'must specify an override id')
    else
      check_property(assignment_overrides_data, 'id', false, all_errors, 'may not specify an override id')
    end
    return nil, all_errors unless all_errors.compact.blank?

    grouped = assignment_overrides_data.group_by {|o| o['assignment_id']}
    assignments = course.active_assignments.where(id: grouped.keys).preload(:assignment_overrides)
    overrides = grouped.map do |assignment_id, overrides_data|
      assignment = assignments.find {|a| a.id.to_s == assignment_id.to_s}
      find_assignment_overrides(assignment, overrides_data.map{|o| o['id']}) if assignment
    end.flatten.compact if for_update

    interpreted = assignment_overrides_data.each_with_index.map do |override_data, i|
      assignment = assignments.find {|a| a.id.to_s == override_data['assignment_id'].to_s}
      unless assignment.present?
        all_errors[i] = ['assignment not found']
        next
      end
      if for_update
        override = overrides.find {|o| o.id.to_s == override_data['id'].to_s}
        unless override.present?
          all_errors[i] = ['override not found']
          next
        end
        set_type = override.set_type
      end

      update_data, errors = interpret_assignment_override_data(assignment, override_data, set_type)
      if errors
        all_errors[i] = errors
        next
      end
      update_data['assignment'] = assignment
      update_data['override'] = override if for_update
      update_data
    end
    all_errors = nil if all_errors.compact.blank?
    [interpreted, all_errors]
  end

  def update_assignment_override_without_save(override, override_data)
    if override_data.key?(:noop_id)
      override.set = nil
      override.set_type = 'Noop'
      override.set_id = override_data[:noop_id]
      override.title = override_data[:title]
    end

    if override_data.key?(:students)
      override.set = nil
      override.set_type = 'ADHOC'

      defunct_student_ids = override.new_record? ?
        Set.new :
        override.assignment_override_students.map(&:user_id).to_set

      override.changed_student_ids = Set.new

      override_data[:students].each do |student|
        if defunct_student_ids.include?(student.id)
          defunct_student_ids.delete(student.id)
        else
          # link will be saved with the override
          link = override.assignment_override_students.build
          link.workflow_state = 'active'
          link.assignment_override = override
          link.user = student
          override.changed_student_ids << student.id
        end
      end

      unless defunct_student_ids.empty?
        override.changed_student_ids.merge(defunct_student_ids)
        override.assignment_override_students.
          where(:user_id => defunct_student_ids.to_a).
          in_batches.
          delete_all
      end
    end

    if override_data.key?(:group)
      override.set = override_data[:group]
    end

    if override_data.key?(:section)
      override.set = override_data[:section]
    end

    if override.set_type == 'ADHOC'
      override.title = override_data[:title] ||
                         (override_data[:students] && override.title_from_students(override_data[:students])) ||
                         override.title
    end

    [:due_at, :unlock_at, :lock_at].each do |field|
      if override_data.key?(field)
        override.send("override_#{field}", override_data[field])
      else
        override.send("clear_#{field}_override")
      end
    end
  end

  def update_assignment_override(override, override_data, updating_user: nil)
    DueDateCacher.with_executing_user(updating_user) do
      override_changed = false
      override.transaction do
        update_assignment_override_without_save(override, override_data)
        override_changed = override.changed? || override.changed_student_ids.present?
        override.save! if override_changed
      end
      if override_changed
        if override.set_type == 'ADHOC' && override.changed_student_ids.present?
          override.assignment.run_if_overrides_changed_later!(
            student_ids: override.changed_student_ids.to_a,
            updating_user: updating_user
          )
        else
          override.assignment.run_if_overrides_changed_later!(updating_user: updating_user)
        end
      end
    end

    true
  rescue ActiveRecord::RecordInvalid
    false
  end

  # updates only the selected overrides; compare with
  # batch_update_assignment_overrides below, which updates
  # all overrides for assignment
  def update_assignment_overrides(overrides, overrides_data, updating_user: nil)
    overrides.zip(overrides_data).each do |override, data|
      update_assignment_override_without_save(override, data)
    end
    return false if overrides.find(&:invalid?).present?

    AssignmentOverride.transaction do
      overrides.each(&:save!)
    end
    overrides.map(&:assignment).uniq.each do |assignment|
      assignment.run_if_overrides_changed_later!(updating_user: updating_user)
    end
  rescue ActiveRecord::RecordInvalid
    false
  end

  def invisible_users_and_overrides_for_user(context, user, existing_overrides)
    # get the student overrides the user can't see and ensure those overrides are included
    visible_user_ids = context.enrollments_visible_to(user).select(:user_id)
    invisible_user_ids = context.enrollments.where.not(:user_id => visible_user_ids).distinct.pluck(:user_id)
    invisible_override_ids = existing_overrides.select{ |ov|
      ov.set_type == 'ADHOC' &&
      !ov.visible_student_overrides(visible_user_ids)
    }.map(&:id)
    return invisible_user_ids, invisible_override_ids
  end

  def update_override_with_invisible_data(override_params, override, invisible_override_ids, invisible_user_ids)
    return override_params = override if invisible_override_ids.include?(override.id)
    # add back in the invisible students for this override if any found
    hidden_ids = override.assignment_override_students.where(user_id: invisible_user_ids).pluck(:user_id)
    unless hidden_ids.empty?
      override_params[:student_ids] = (override_params[:student_ids] + hidden_ids)
      overrides_size = override_params[:student_ids].size
      override_params[:title] = t({ one: '1 student', other: "%{count} students" }, count: overrides_size)
    end
  end

  def prepare_assignment_overrides_for_batch_update(assignment, overrides_params, user)
    existing_overrides = assignment.assignment_overrides.active
    invisible_user_ids, invisible_override_ids = invisible_users_and_overrides_for_user(
      assignment.context, user, existing_overrides
    )

    override_param_ids = invisible_override_ids + overrides_params.map {|ov| ov[:id].to_i}
    split_overrides = existing_overrides.group_by do |override|
      override_param_ids.include?(override.id) ? :keep : :delete
    end

    overrides_to_keep = split_overrides[:keep] || []
    overrides_to_delete = split_overrides[:delete] || []

    ActiveRecord::Associations::Preloader.new.preload(overrides_to_keep, :assignment_override_students)

    override_errors = []
    overrides_to_save = overrides_params.map do |override_params|
      override = get_override_from_params(override_params, assignment, overrides_to_keep)
      update_override_with_invisible_data(override_params, override, invisible_override_ids, invisible_user_ids)

      data, errors = interpret_assignment_override_data(assignment, override_params, override.set_type)
      if errors
        override_errors << errors.join(',')
      else
        update_assignment_override_without_save(override, data)
      end
      override
    end

    overrides_to_create, overrides_to_update = overrides_to_save.partition(&:new_record?)

    {
      overrides_to_create: overrides_to_create,
      overrides_to_update: overrides_to_update,
      overrides_to_delete: overrides_to_delete,
      override_errors: override_errors
    }
  end

  def perform_batch_update_assignment_overrides(assignment, prepared_overrides, updating_user: nil)
    prepared_overrides[:override_errors].each do |error|
      assignment.errors.add(:base, error)
    end

    raise ActiveRecord::RecordInvalid.new(assignment) if assignment.errors.any?

    if prepared_overrides[:overrides_to_delete].any?
      assignment.assignment_overrides.where(id: prepared_overrides[:overrides_to_delete]).destroy_all
    end

    raise ActiveRecord::RecordInvalid.new(assignment) unless assignment.valid?

    prepared_overrides[:overrides_to_create].each(&:save!)
    prepared_overrides[:overrides_to_update].each(&:save!)

    @overrides_affected = prepared_overrides[:overrides_to_delete].size +
      prepared_overrides[:overrides_to_create].size + prepared_overrides[:overrides_to_update].size

    assignment.touch # invalidate cached list of overrides for the assignment
    assignment.assignment_overrides.reset # unload the obsolete association
    assignment.run_if_overrides_changed_later!(updating_user: updating_user)
  end

  def batch_update_assignment_overrides(assignment, overrides_params, user)
    prepared_overrides = prepare_assignment_overrides_for_batch_update(assignment, overrides_params, user)
    perform_batch_update_assignment_overrides(assignment, prepared_overrides, updating_user: user)
  end

  def get_override_from_params(override_params, assignment, potential_overrides)
    override = potential_overrides.detect {|ov| ov.id == override_params[:id].to_i}
    return override if override

    case assignment
    when Assignment
      AssignmentOverride.new(assignment_id: assignment.id, dont_touch_assignment: true)
    when Quizzes::Quiz
      AssignmentOverride.new(quiz_id: assignment.id, dont_touch_assignment: true)
    end
  end
  private :get_override_from_params

  def deserialize_overrides(overrides)
    if overrides.is_a?(Hash) || overrides.is_a?(ActionController::Parameters)
      return unless overrides.keys.all?{ |k| k.to_i.to_s == k.to_s }
      indices = overrides.keys.sort_by(&:to_i)
      return unless indices.map(&:to_i) == (0...indices.size).to_a
      overrides = indices.map{ |index| overrides[index] }
    else
      overrides
    end
  end
end
