# 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/>.
#

# @API Assignments
# @subtopic Assignment Overrides
#
# @model AssignmentOverride
#     {
#       "id": "AssignmentOverride",
#       "description": "",
#       "properties": {
#         "id": {
#           "description": "the ID of the assignment override",
#           "example": 4,
#           "type": "integer"
#         },
#         "assignment_id": {
#           "description": "the ID of the assignment the override applies to (present if the override applies to an assignment)",
#           "example": 123,
#           "type": "integer"
#         },
#         "quiz_id": {
#           "description": "the ID of the quiz the override applies to (present if the override applies to a quiz)",
#           "example": 123,
#           "type": "integer"
#         },
#         "context_module_id": {
#           "description": "the ID of the module the override applies to (present if the override applies to a module)",
#           "example": 123,
#           "type": "integer"
#         },
#         "discussion_topic_id": {
#           "description": "the ID of the discussion the override applies to (present if the override applies to an ungraded discussion)",
#           "example": 123,
#           "type": "integer"
#         },
#         "wiki_page_id": {
#           "description": "the ID of the page the override applies to (present if the override applies to a page)",
#           "example": 123,
#           "type": "integer"
#         },
#         "attachment_id": {
#           "description": "the ID of the file the override applies to (present if the override applies to a file)",
#           "example": 123,
#           "type": "integer"
#         },
#         "student_ids": {
#           "description": "the IDs of the override's target students (present if the override targets an ad-hoc set of students)",
#           "example": [1, 2, 3],
#           "type": "array",
#           "items": {"type": "integer"}
#         },
#         "group_id": {
#           "description": "the ID of the override's target group (present if the override targets a group and the assignment is a group assignment)",
#           "example": 2,
#           "type": "integer"
#         },
#         "course_section_id": {
#           "description": "the ID of the overrides's target section (present if the override targets a section)",
#           "example": 1,
#           "type": "integer"
#         },
#         "title": {
#           "description": "the title of the override",
#           "example": "an assignment override",
#           "type": "string"
#         },
#         "due_at": {
#           "description": "the overridden due at (present if due_at is overridden)",
#           "example": "2012-07-01T23:59:00-06:00",
#           "type": "datetime"
#         },
#         "all_day": {
#           "description": "the overridden all day flag (present if due_at is overridden)",
#           "example": true,
#           "type": "boolean"
#         },
#         "all_day_date": {
#           "description": "the overridden all day date (present if due_at is overridden)",
#           "example": "2012-07-01",
#           "type": "datetime"
#         },
#         "unlock_at": {
#           "description": "the overridden unlock at (present if unlock_at is overridden)",
#           "example": "2012-07-01T23:59:00-06:00",
#           "type": "datetime"
#         },
#         "lock_at": {
#           "description": "the overridden lock at, if any (present if lock_at is overridden)",
#           "example": "2012-07-01T23:59:00-06:00",
#           "type": "datetime"
#         }
#       }
#     }
#
class AssignmentOverridesController < ApplicationController
  before_action :require_group, only: :group_alias
  before_action :require_section, only: :section_alias
  before_action :require_course
  before_action :require_assignment, except: %i[batch_retrieve batch_update batch_create]
  before_action :require_assignment_edit, only: %i[create update destroy]
  before_action :require_all_assignments_edit, only: [:batch_update, :batch_create]
  before_action :require_override, only: %i[show update destroy]

  include Api::V1::AssignmentOverride

  # @API List assignment overrides
  #
  # Returns the paginated list of overrides for this assignment that target
  # sections/groups/students visible to the current user.
  #
  # @returns [AssignmentOverride]
  def index
    @overrides = assignment_override_collection(@assignment, include_students: true)
    render json: assignment_overrides_json(@overrides, @current_user)
  end

  # @API Get a single assignment override
  #
  # Returns details of the the override with the given id.
  #
  # @returns AssignmentOverride
  def show
    render json: assignment_override_json(@override)
  end

  # @API Redirect to the assignment override for a group
  #
  # Responds with a redirect to the override for the given group, if any
  # (404 otherwise).
  def group_alias
    @override = find_assignment_override(@assignment, @group)
    raise ActiveRecord::RecordNotFound unless @override

    redirect_to api_v1_assignment_override_url(
      course_id: @course.id,
      assignment_id: @assignment.id,
      id: @override
    )
  end

  # @API Redirect to the assignment override for a section
  #
  # Responds with a redirect to the override for the given section, if any
  # (404 otherwise).
  def section_alias
    @override = find_assignment_override(@assignment, @section)
    raise ActiveRecord::RecordNotFound unless @override

    redirect_to api_v1_assignment_override_url(
      course_id: @course.id,
      assignment_id: @assignment.id,
      id: @override
    )
  end

  # @API Create an assignment override
  #
  # @argument assignment_override[student_ids][] [Integer] The IDs of
  #   the override's target students. If present, the IDs must each identify a
  #   user with an active student enrollment in the course that is not already
  #   targetted by a different adhoc override.
  #
  # @argument assignment_override[title] The title of the adhoc
  #   assignment override. Required if student_ids is present, ignored
  #   otherwise (the title is set to the name of the targetted group or section
  #   instead).
  #
  # @argument assignment_override[group_id] [Integer] The ID of the
  #   override's target group. If present, the following conditions must be met
  #   for the override to be successful:
  #
  #   1. the assignment MUST be a group assignment (a group_category_id is assigned to it)
  #   2. the ID must identify an active group in the group set the assignment is in
  #   3. the ID must not be targetted by a different override
  #
  #   See {Appendix: Group assignments} for more info.
  #
  # @argument assignment_override[course_section_id] [Integer] The ID
  #   of the override's target section. If present, must identify an active
  #   section of the assignment's course not already targetted by a different
  #   override.
  #
  # @argument assignment_override[due_at] [DateTime] The day/time
  #   the overridden assignment is due. Accepts times in ISO 8601 format, e.g.
  #   2014-10-21T18:48:00Z. If absent, this override will not affect due date.
  #   May be present but null to indicate the override removes any previous due
  #   date.
  #
  # @argument assignment_override[unlock_at] [DateTime] The day/time
  #   the overridden assignment becomes unlocked. Accepts times in ISO 8601
  #   format, e.g. 2014-10-21T18:48:00Z. If absent, this override will not
  #   affect the unlock date. May be present but null to indicate the override
  #   removes any previous unlock date.
  #
  # @argument assignment_override[lock_at] [DateTime] The day/time
  #   the overridden assignment becomes locked. Accepts times in ISO 8601
  #   format, e.g. 2014-10-21T18:48:00Z. If absent, this override will not
  #   affect the lock date. May be present but null to indicate the override
  #   removes any previous lock date.
  #
  # One of student_ids, group_id, or course_section_id must be present. At most
  # one should be present; if multiple are present only the most specific
  # (student_ids first, then group_id, then course_section_id) is used and any
  # others are ignored.
  #
  # @returns AssignmentOverride
  #
  # @example_request
  #
  #   curl 'https://<canvas>/api/v1/courses/1/assignments/2/overrides.json' \
  #        -X POST \
  #        -F 'assignment_override[student_ids][]=8' \
  #        -F 'assignment_override[title]=Fred Flinstone' \
  #        -F 'assignment_override[due_at]=2012-10-08T21:00:00Z' \
  #        -H "Authorization: Bearer <token>"
  #
  def create
    @override = @assignment.assignment_overrides.build

    data, errors = interpret_assignment_override_data(@assignment, params[:assignment_override])
    return bad_request(errors:) if errors

    if update_assignment_override(@override, data, updating_user: @current_user)
      render json: assignment_override_json(@override), status: :created
    else
      bad_request(@override.errors)
    end
  end

  # @API Update an assignment override
  #
  # @argument assignment_override[student_ids][] [Integer] The IDs of the
  #   override's target students. If present, the IDs must each identify a
  #   user with an active student enrollment in the course that is not already
  #   targetted by a different adhoc override. Ignored unless the override
  #   being updated is adhoc.
  #
  # @argument assignment_override[title] [String] The title of an adhoc
  #   assignment override. Ignored unless the override being updated is adhoc.
  #
  # @argument assignment_override[due_at] [DateTime] The day/time
  #   the overridden assignment is due. Accepts times in ISO 8601 format, e.g.
  #   2014-10-21T18:48:00Z. If absent, this override will not affect due date.
  #   May be present but null to indicate the override removes any previous due
  #   date.
  #
  # @argument assignment_override[unlock_at] [DateTime] The day/time
  #   the overridden assignment becomes unlocked. Accepts times in ISO 8601
  #   format, e.g. 2014-10-21T18:48:00Z. If absent, this override will not
  #   affect the unlock date. May be present but null to indicate the override
  #   removes any previous unlock date.
  #
  # @argument assignment_override[lock_at] [DateTime] The day/time
  #   the overridden assignment becomes locked. Accepts times in ISO 8601
  #   format, e.g. 2014-10-21T18:48:00Z. If absent, this override will not
  #   affect the lock date. May be present but null to indicate the override
  #   removes any previous lock date.
  #
  # All current overridden values must be supplied if they are to be retained;
  # e.g. if due_at was overridden, but this PUT omits a value for due_at,
  # due_at will no longer be overridden. If the override is adhoc and
  # student_ids is not supplied, the target override set is unchanged. Target
  # override sets cannot be changed for group or section overrides.
  #
  # @returns AssignmentOverride
  #
  # @example_request
  #
  #   curl 'https://<canvas>/api/v1/courses/1/assignments/2/overrides/3.json' \
  #        -X PUT \
  #        -F 'assignment_override[title]=Fred Flinstone' \
  #        -F 'assignment_override[due_at]=2012-10-08T21:00:00Z' \
  #        -H "Authorization: Bearer <token>"
  #
  def update
    data, errors = interpret_assignment_override_data(@assignment, params[:assignment_override], @override.set_type)
    return bad_request(errors:) if errors

    if update_assignment_override(@override, data, updating_user: @current_user)
      render json: assignment_override_json(@override)
    else
      bad_request(@override.errors)
    end
  end

  # @API Delete an assignment override
  #
  # Deletes an override and returns its former details.
  #
  # @returns AssignmentOverride
  #
  # @example_request
  #
  #   curl 'https://<canvas>/api/v1/courses/1/assignments/2/overrides/3.json' \
  #        -X DELETE \
  #        -H "Authorization: Bearer <token>"
  #
  def destroy
    if @override.destroy
      render json: assignment_override_json(@override)
    else
      bad_request(@override.errors)
    end
  end

  # @API Batch retrieve overrides in a course
  #
  # Returns a list of specified overrides in this course, providing
  # they target sections/groups/students visible to the current user.
  # Returns null elements in the list for requests that were not found.
  #
  # @argument assignment_overrides[][id] [Required, String] Ids of overrides to retrieve
  #
  # @argument assignment_overrides[][assignment_id] [Required, String] Ids of assignments for each override
  #
  # @returns [AssignmentOverride]
  #
  # @example_request
  #
  #   curl 'https://<canvas>/api/v1/courses/12/assignments/overrides.json?assignment_overrides[][id]=109&assignment_overrides[][assignment_id]=122&assignment_overrides[][id]=99&assignment_overrides[][assignment_id]=111' \
  #        -H "Authorization: Bearer <token>"
  #
  def batch_retrieve
    # check request format
    override_params = deserialize_overrides(params[:assignment_overrides])
    if override_params.blank?
      return bad_request(errors: ["no assignment_overrides values present"])
    elsif !override_params.is_a?(Array) ||
          !override_params.all? { |o| o.is_a?(ActionController::Parameters) && o.key?("assignment_id") && o.key?("id") }
      return bad_request(errors: ["must specify an array with entry format { id, assignment_id }"])
    end

    all_requests = override_params.group_by { |req| req["assignment_id"].to_i }
    assignments = @course.active_assignments.where(id: all_requests.keys).preload(:assignment_overrides)

    overrides = all_requests.map do |assignment_id, requests|
      override_ids = requests.map { |r| r["id"].to_i }
      assignment = assignments.find { |a| a.id == assignment_id }
      next unless assignment

      find_assignment_overrides(assignment, override_ids)
    end.flatten.compact

    # reorder to match request
    sorted = override_params.map do |req|
      overrides.find do |o|
        o.id == req["id"].to_i
      end
    end

    render json: assignment_overrides_json(sorted, @current_user)
  end

  # @API Batch create overrides in a course
  #
  # Creates the specified overrides for each assignment.  Handles creation in a
  # transaction, so all records are created or none are.
  #
  # One of student_ids, group_id, or course_section_id must be present. At most
  # one should be present; if multiple are present only the most specific
  # (student_ids first, then group_id, then course_section_id) is used and any
  # others are ignored.
  #
  # Errors are reported in an errors attribute, an array of errors corresponding
  # to inputs.  Global errors will be reported as a single element errors array
  #
  # @argument assignment_overrides[] [Required, AssignmentOverride] Attributes for the new assignment overrides.
  #     See {api:AssignmentOverridesController#create Create an assignment override} for available
  #     attributes
  #
  # @returns [AssignmentOverride]
  #
  # @example_request
  #
  #   curl "https://<canvas>/api/v1/courses/12/assignments/overrides.json" \
  #        -X POST \
  #        -F "assignment_overrides[][assignment_id]=109" \
  #        -F 'assignment_overrides[][student_ids][]=8' \
  #        -F "assignment_overrides[][title]=foo" \
  #        -F "assignment_overrides[][assignment_id]=13" \
  #        -F "assignment_overrides[][course_section_id]=200" \
  #        -F "assignment_overrides[][due_at]=2012-10-08T21:00:00Z" \
  #        -H "Authorization: Bearer <token>"
  #
  def batch_create
    batch_edit(false)
  end

  # @API Batch update overrides in a course
  #
  # Updates a list of specified overrides for each assignment.  Handles overrides
  # in a transaction, so either all updates are applied or none.
  # See {api:AssignmentOverridesController#update Update an assignment override} for
  # available attributes.
  #
  # All current overridden values must be supplied if they are to be retained;
  # e.g. if due_at was overridden, but this PUT omits a value for due_at,
  # due_at will no longer be overridden. If the override is adhoc and
  # student_ids is not supplied, the target override set is unchanged. Target
  # override sets cannot be changed for group or section overrides.
  #
  # Errors are reported in an errors attribute, an array of errors corresponding
  # to inputs.  Global errors will be reported as a single element errors array
  #
  # @argument assignment_overrides[] [Required, AssignmentOverride] Attributes for the updated overrides.
  #
  # @returns [AssignmentOverride]
  #
  # @example_request
  #
  #   curl "https://<canvas>/api/v1/courses/12/assignments/overrides.json" \
  #        -X PUT \
  #        -F "assignment_overrides[][id]=122" \
  #        -F "assignment_overrides[][assignment_id]=109" \
  #        -F "assignment_overrides[][title]=foo" \
  #        -F "assignment_overrides[][id]=993" \
  #        -F "assignment_overrides[][assignment_id]=13" \
  #        -F "assignment_overrides[][due_at]=2012-10-08T21:00:00Z" \
  #        -H "Authorization: Bearer <token>"
  #
  def batch_update
    batch_edit(true)
  end

  protected

  def require_group
    @group = find_group(nil, params[:group_id])
    @course = @group.context
  end

  def require_section
    @section = find_section(nil, params[:course_section_id])
    @course = @section.course
  end

  def require_course
    @course ||= api_find(Course.active, params[:course_id])
    raise ActiveRecord::RecordNotFound if @course.deleted?

    @context = @course
    authorized_action(@course, @current_user, :read)
  end

  def require_assignment
    @assignment = api_find(@course.active_assignments, params[:assignment_id])
  end

  def require_assignment_edit
    authorized_action(@assignment, @current_user, :update)
  end

  def require_all_assignments_edit
    authorized_action(@course, @current_user, :manage_assignments_edit)
  end

  def require_override
    @override = find_assignment_override(@assignment, params[:id])
    raise ActiveRecord::RecordNotFound unless @override # i.e. if params[:id] was nil
  end

  def bad_request(errors)
    render json: errors, status: :bad_request
  end

  def batch_edit(is_update)
    override_params = deserialize_overrides(params[:assignment_overrides])
    all_data, all_errors = interpret_batch_assignment_overrides_data(@course, override_params, is_update)
    return bad_request(errors: all_errors) if all_errors.present?

    overrides = all_data.map do |data|
      is_update ? data["override"] : data["assignment"].assignment_overrides.build
    end

    if update_assignment_overrides(overrides, all_data, updating_user: @current_user)
      render json: assignment_overrides_json(overrides, @current_user)
    else
      errors = overrides.map do |override|
        override.errors.presence
      end
      errors = ["unknown error"] unless errors.compact.present?
      bad_request(errors:)
    end
  end

  # @!appendix Group assignments
  #
  #  {include:file:doc/examples/group_assignment.md}
  #
  #  @see api:AssignmentOverridesController#create Creating an assignment override
  #  @see api:AssignmentsApiController#create Creating an assignment
  #  @see api:Assignments:Assignment
end
