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

require "icalendar"

Icalendar::Event.optional_property :x_alt_desc

class CalendarEvent < ActiveRecord::Base
  include CopyAuthorizedLinks
  include TextHelper
  include HtmlTextHelper
  include Plannable

  include MasterCourses::Restrictor

  restrict_columns :content, [:title, :description]
  restrict_columns :settings, %i[location_name location_address start_at end_at all_day all_day_date series_uuid rrule]

  attr_accessor :cancel_reason, :imported
  attr_accessor :update_all

  sanitize_field :description, CanvasSanitize::SANITIZE
  copy_authorized_links(:description) { [effective_context, nil] }

  include Workflow
  include LinkedAttachmentHandler

  def self.html_fields
    %w[description]
  end

  def update_attachment_associations
    return if series_uuid.present? && !series_head

    super
  end

  PERMITTED_ATTRIBUTES = %i[title
                            description
                            start_at
                            end_at
                            location_name
                            location_address
                            time_zone_edited
                            cancel_reason
                            participants_per_appointment
                            remove_child_events
                            all_day
                            comments
                            context_code
                            important_dates
                            series_uuid
                            rrule
                            blackout_date].freeze
  def self.permitted_attributes
    PERMITTED_ATTRIBUTES
  end

  belongs_to :context,
             polymorphic: %i[course user group appointment_group course_section account],
             polymorphic_prefix: true
  belongs_to :user
  belongs_to :parent_event, class_name: "CalendarEvent", foreign_key: :parent_calendar_event_id, inverse_of: :child_events
  has_many :child_events, -> { where.not(workflow_state: "deleted") }, class_name: "CalendarEvent", foreign_key: :parent_calendar_event_id, inverse_of: :parent_event
  belongs_to :web_conference, autosave: true
  belongs_to :root_account, class_name: "Account"
  has_many :attachment_associations, as: :context, inverse_of: :context

  validates :context, :workflow_state, presence: true
  validates_associated :context, if: ->(record) { record.validate_context }
  validates :description, length: { maximum: maximum_long_text_length, allow_blank: true }
  validates :title, length: { maximum: maximum_string_length, allow_blank: true }
  validates :comments, length: { maximum: 255, allow_blank: true }
  validate :validate_conference_visibility
  before_create :set_root_account
  before_save :default_values
  after_save :touch_context
  after_save :replace_child_events
  after_save :sync_parent_event
  after_save :sync_conference
  after_update :sync_child_events

  # when creating/updating a calendar_event, you can give it a list of child
  # events. these will update/replace any existing child events. the format is:
  # [{:start_at => start_at, :end_at => end_at, :context_code => context_code},
  #  {:start_at => start_at, :end_at => end_at, :context_code => context_code},
  #  ...]
  # the context for each child event must have this event's context as its
  # parent_event_context, and there can only be one event per context.
  # remove_child_events can be set to remove all existing events (since rails
  # form handling mechanism doesn't let you specify an empty array)
  attr_accessor :child_event_data, :remove_child_events, :child_event_contexts

  validates_each :child_event_data do |record, attr, events|
    next unless events || Canvas::Plugin.value_to_boolean(record.remove_child_events)

    events ||= []
    events = events.values if events.is_a?(Hash)
    next record.errors.add(attr, t("errors.no_updating_user", "Can't update child events unless an updating_user is set")) if events.present? && !record.updating_user

    context_codes = events.pluck(:context_code)
    next record.errors.add(attr, t("errors.duplicate_child_event_contexts", "Duplicate child event contexts")) if context_codes != context_codes.uniq

    contexts = find_all_by_asset_string(context_codes).group_by(&:asset_string)
    context_codes.each do |code|
      context = contexts[code] && contexts[code][0]
      new_event = events.detect { |e| e[:context_code] == context&.asset_string }
      existing_event = record.child_events.where(context:).first
      event_unchanged = new_event && existing_event && Time.zone.parse(new_event[:start_at]) == existing_event.start_at && Time.zone.parse(new_event[:end_at]) == existing_event.end_at
      next if (context&.grants_right?(record.updating_user, :manage_calendar) || event_unchanged) && context.try(:parent_event_context) == record.context

      break record.errors.add(attr, t("errors.invalid_child_event_context", "Invalid child event context"))
    end
    record.child_event_contexts = contexts
    record.child_event_data = events
  end

  def replace_child_events
    return unless @child_event_data

    current_events = child_events.group_by { |e| e[:context_code] }
    @child_event_data.each do |data|
      if (event = current_events.delete(data[:context_code])&.first)
        event.updating_user = @updating_user
        event.saving_user = @updating_user
        event.update(start_at: data[:start_at], end_at: data[:end_at])
      else
        context = @child_event_contexts[data[:context_code]][0]
        event = child_events.build(start_at: data[:start_at], end_at: data[:end_at])
        event.updating_user = @updating_user
        event.saving_user = @updating_user
        event.context = context
        event.skip_sync_parent_event = true
        event.save
      end
    end

    current_events.values.flatten.each(&:destroy)
    cache_child_event_ranges!
    @child_event_data = nil
  end

  def hidden?
    !appointment_group? && !child_events.empty?
  end

  def in_a_series?
    !!series_uuid
  end

  def series_tail?
    in_a_series? && !series_head
  end

  def effective_context
    (effective_context_code && ActiveRecord::Base.find_all_by_asset_string(effective_context_code).first) || context
  end

  scope :active, -> { where("calendar_events.workflow_state<>'deleted'") }
  scope :are_locked, -> { where(workflow_state: "locked") }
  scope :are_unlocked, -> { where("calendar_events.workflow_state NOT IN ('deleted', 'locked')") }

  # controllers/apis/etc. should generally use for_user_and_context_codes instead
  scope :for_context_codes, ->(codes) { where(context_code: codes) }

  # appointments and appointment_participants have the appointment_group and
  # the user as the context, respectively. we are actually interested in
  # grouping them under the effective context (i.e. appointment_group.context).
  # it's the responsibility of the caller to ensure the user has rights to the
  # specified codes (e.g. using User#appointment_context_codes)
  scope :for_user_and_context_codes, lambda { |user, codes, section_codes = nil|
    section_codes ||= user.section_context_codes(codes)
    effectively_courses_codes = [user.asset_string] + section_codes
    # the all_codes check is redundant, but makes the query more efficient
    all_codes = codes | effectively_courses_codes

    group_codes = codes.grep(/\Aappointment_group_\d+\z/)
    codes -= group_codes

    or_clauses = []
    unscoped do
      # explicit contexts (e.g. course_123)
      or_clauses << where(context_code: codes, effective_context_code: nil) unless codes.empty?

      # appointments (manageable or reservable)
      or_clauses << where(context_code: group_codes) unless group_codes.empty?

      # own appointment_participants, or section events in the course
      unless codes.empty?
        this_scope = where(context_code: effectively_courses_codes)

        codes_conditions = codes.map do |code|
          wildcard(quoted_table_name + ".effective_context_code", code, delimiter: ",")
        end.join(" OR ")

        this_scope = this_scope.where(codes_conditions)
        or_clauses << this_scope
      end
    end

    next none if or_clauses.empty?
    next merge(or_clauses.first) if or_clauses.length == 1

    # basically we're forming "context_code IN (<all_codes>) AND (... OR ... OR ...)"
    result = where(context_code: all_codes)
    or_clauses_merged = or_clauses.first
    or_clauses[1..].each do |clause|
      or_clauses_merged = or_clauses_merged.or(clause)
    end

    result = result.merge(or_clauses_merged)
    result
  }

  scope :not_hidden, lambda {
    where("NOT EXISTS (
      SELECT id
      FROM #{CalendarEvent.quoted_table_name} sub_events
      WHERE sub_events.parent_calendar_event_id=calendar_events.id
        AND sub_events.workflow_state <> 'deleted'
    )")
  }

  scope :undated, -> { where(start_at: nil, end_at: nil) }

  scope :between, ->(start, ending) { where(start_at: ..ending, end_at: start..) }
  scope :current, -> { where("calendar_events.end_at>=?", Time.zone.now) }
  scope :updated_after, lambda { |*args|
    if args.first
      where("calendar_events.updated_at IS NULL OR calendar_events.updated_at>?", args.first)
    else
      all
    end
  }

  scope :events_without_child_events, -> { where("NOT EXISTS (SELECT 1 FROM #{CalendarEvent.quoted_table_name} children WHERE children.parent_calendar_event_id = calendar_events.id AND children.workflow_state<>'deleted')") }
  scope :events_with_child_events, -> { where("EXISTS (SELECT 1 FROM #{CalendarEvent.quoted_table_name} children WHERE children.parent_calendar_event_id = calendar_events.id AND children.workflow_state<>'deleted')") }

  scope :user_created, -> { where(timetable_code: nil) }
  scope :for_timetable, -> { where.not(timetable_code: nil) }

  scope :with_important_dates, -> { where(important_dates: true) }
  scope :with_blackout_date, -> { where(blackout_date: true) }
  scope :valid_ranges, -> { where.not(start_at: nil).where.not(end_at: nil) }

  def validate_context!
    @validate_context = true
    context.validation_event_override = self
  end
  attr_reader :validate_context

  def default_values
    self.context_code = "#{context_type.underscore}_#{context_id}"
    self.title ||= (context_type.to_s + " Event")

    populate_missing_dates
    populate_all_day_flag unless imported

    if parent_event
      populate_with_parent_event
    elsif context.is_a?(AppointmentGroup)
      populate_appointment_group_defaults
    end
  end
  protected :default_values

  def set_root_account(ctx = context)
    if ctx.respond_to?(:root_account)
      self.root_account = ctx.root_account # course, section, group
    else
      case ctx
      when User
        if effective_context.is_a?(User)
          self.root_account_id = 0
        else
          set_root_account(effective_context)
        end
      when AppointmentGroup
        self.root_account = context.context&.root_account
      end
    end
  end

  def child_event_participants_scope
    shard.activate do
      # user is set directly, or context is user
      User.where("id IN
        (#{child_events.where.not(user_id: nil).select(:user_id).to_sql}
        UNION
         #{child_events.where(user_id: nil, context_type: "User").select(:context_id).to_sql})")
    end
  end

  def populate_appointment_group_defaults
    self.effective_context_code = context.appointment_group_contexts.map(&:context_code).join(",")
    if new_record?
      AppointmentGroup::EVENT_ATTRIBUTES.each { |attr| send(:"#{attr}=", context.send(attr)) }
      if locked?
        self.start_at = start_at_was if !new_record? && start_at_changed?
        self.end_at   = end_at_was   if !new_record? && end_at_changed?
      end
    else
      # we only allow changing the description
      (AppointmentGroup::EVENT_ATTRIBUTES - [:description]).each { |attr| send(:"#{attr}=", send(:"#{attr}_was")) if send(:"#{attr}_changed?") }
    end
  end
  protected :populate_appointment_group_defaults

  def populate_with_parent_event
    self.effective_context_code = if appointment_group # appointment participant
                                    appointment_group.appointment_group_contexts.map(&:context_code).join(",") if appointment_group.participant_type == "User"
                                  else # e.g. section-level event
                                    parent_event.context_code
                                  end
    (locked? ? LOCKED_ATTRIBUTES : CASCADED_ATTRIBUTES).each { |attr| send(:"#{attr}=", parent_event.send(attr)) }
  end
  protected :populate_with_parent_event

  # Populate the start and end dates if they are not set, or if they are invalid
  def populate_missing_dates
    self.end_at ||= start_at
    self.start_at ||= self.end_at
    if self.start_at && self.end_at && self.end_at < self.start_at
      self.end_at = self.start_at
    end
  end
  protected :populate_missing_dates

  def populate_all_day_flag
    # If the all day flag has been changed to all day, set the times to 00:00
    if all_day_changed? && all_day?
      self.start_at = zoned_start_at&.beginning_of_day
      self.end_at = zoned_end_at&.beginning_of_day
    elsif start_at_changed? || end_at_changed? || Canvas::Plugin.value_to_boolean(remove_child_events)
      self.all_day = self.start_at && self.start_at == self.end_at && zoned_start_at.strftime("%H:%M") == "00:00"
    end

    if all_day && (!all_day_date || start_at_changed? || all_day_date_changed?)
      self.start_at = zoned_start_at&.beginning_of_day
      self.end_at = zoned_end_at&.beginning_of_day
      self.all_day_date = zoned_start_at&.to_date
    end
  end
  protected :populate_all_day_flag

  # Localized start_at
  def zoned_start_at
    start_at&.in_time_zone(edited_in_zone)
  end

  def zoned_end_at
    end_at&.in_time_zone(edited_in_zone)
  end

  def edited_in_zone
    (time_zone_edited && ActiveSupport::TimeZone[time_zone_edited]) || Time.zone
  end

  CASCADED_ATTRIBUTES = %i[
    title
    description
    location_name
    location_address
    web_conference
    important_dates
  ].freeze
  LOCKED_ATTRIBUTES = CASCADED_ATTRIBUTES + [
    :start_at,
    :end_at
  ].freeze

  def sync_child_events
    locked_changes = LOCKED_ATTRIBUTES.select { |attr| saved_change_to_attribute?(attr) }
    cascaded_changes = CASCADED_ATTRIBUTES.select { |attr| saved_change_to_attribute?(attr) }
    child_events.are_locked.update_all(locked_changes.index_with { |attr| send(attr) }) if locked_changes.present?
    child_events.are_unlocked.update_all(cascaded_changes.index_with { |attr| send(attr) }) if cascaded_changes.present?
  end

  def sync_conference
    return if web_conference_id.blank?

    if saved_change_to_title?
      web_conference.title = title
    end
    if saved_change_to_start_at? && web_conference.user_settings.key?(:scheduled_date)
      web_conference.user_settings[:scheduled_date] = start_at
    end
    unless web_conference.lti?
      web_conference.invite_users_from_context
    end

    web_conference.save!
  end

  attr_writer :skip_sync_parent_event

  def sync_parent_event
    return unless parent_event
    return if appointment_group
    return unless saved_change_to_start_at? || saved_change_to_end_at? || saved_change_to_workflow_state?
    return if @skip_sync_parent_event

    parent_event.cache_child_event_ranges! unless workflow_state == "deleted"
  end

  def cache_child_event_ranges!
    events = child_events.reload

    if events.present?
      CalendarEvent.where(id: self)
                   .update_all(start_at: events.filter_map(&:start_at).min,
                               end_at: events.filter_map(&:end_at).max)
      reload
    end
  end

  workflow do
    state :active
    state :locked do # locked events may only be deleted, they cannot be edited directly
      event :unlock, transitions_to: :active
    end
    state :deleted
  end

  alias_method :destroy_permanently!, :destroy
  def destroy(update_context_or_parent = true)
    transaction do
      self.workflow_state = "deleted"
      self.deleted_at = Time.now.utc
      self.web_conference = nil
      save!
      child_events.find_each do |e|
        e.cancel_reason = cancel_reason
        e.updating_user = updating_user
        e.destroy(false)
      end
      next unless update_context_or_parent

      if appointment_group
        context.touch if context_type == "AppointmentGroup" # ensures end_at/start_at get updated
        # when deleting an appointment or appointment_participant, make sure we reset the cache
        appointment_group.clear_cached_available_slots!
        appointment_group.save!
      end
      if parent_event && parent_event.child_events.empty?
        parent_event.class.suspend_callbacks(:update_attachment_associations) do
          parent_event.workflow_state = parent_event.locked? ? "active" : "deleted"
          parent_event.save!
        end
      end
    end
    true
  end

  def time_zone_edited
    CGI.unescapeHTML(super || "")
  end

  has_a_broadcast_policy

  def course_broadcast_data
    return appointment_group.broadcast_data if appointment_group

    if context.respond_to?(:broadcast_data)
      context.broadcast_data
    else
      {}
    end
  end

  set_broadcast_policy do
    dispatch :new_event_created
    to { participants(include_observers: true) - [@updating_user] }
    whenever do
      !appointment_group && !account && context.available? && previously_new_record? && !hidden? && !series_tail?
    end
    data { course_broadcast_data }

    dispatch :event_date_changed
    to { participants(include_observers: true) - [@updating_user] }
    whenever do
      !appointment_group &&
        !account &&
        context.available? && (
        changed_in_state(:active, fields: :start_at) ||
        changed_in_state(:active, fields: :end_at)
      ) && !hidden? && (!in_a_series? || (update_all && !series_tail?))
    end
    data { course_broadcast_data }

    dispatch :appointment_reserved_by_user
    to do
      appointment_group.instructors +
        User.observing_students_in_course(@updating_user.id, appointment_group.active_contexts.select { |c| c.is_a?(Course) })
    end
    whenever do
      @updating_user && appointment_group && parent_event &&
        previously_new_record? &&
        context == appointment_group.participant_for(@updating_user)
    end
    data { { updating_user_name: @updating_user.name }.merge(course_broadcast_data) }

    dispatch :appointment_canceled_by_user
    to do
      appointment_group.instructors +
        User.observing_students_in_course(@updating_user.id, appointment_group.active_contexts.select { |c| c.is_a?(Course) })
    end
    whenever do
      appointment_group && parent_event &&
        deleted? &&
        saved_change_to_workflow_state? &&
        @updating_user &&
        context == appointment_group.participant_for(@updating_user)
    end
    data { { updating_user_name: @updating_user.name, cancel_reason: @cancel_reason }.merge(course_broadcast_data) }

    dispatch :appointment_reserved_for_user
    to { participants(include_observers: true) - [@updating_user] }
    whenever do
      appointment_group && parent_event &&
        previously_new_record?
    end
    data { { updating_user_name: @updating_user.name }.merge(course_broadcast_data) }

    dispatch :appointment_deleted_for_user
    to { participants(include_observers: true) - [@updating_user] }
    whenever do
      appointment_group && parent_event &&
        deleted? &&
        saved_change_to_workflow_state?
    end
    data { { updating_user_name: @updating_user.name, cancel_reason: @cancel_reason }.merge(course_broadcast_data) }
  end

  def participants(include_observers: false)
    if context_type == "User"
      if appointment_group? && include_observers
        course_ids = appointment_group.appointment_group_contexts.where(context_type: "Course").pluck(:context_id)
        [context] + User.observing_students_in_course(context, course_ids)
      else
        [context]
      end
    elsif context.respond_to?(:participants)
      context.participants(include_observers:, by_date: true)
    else
      []
    end
  end

  def updating_user=(user)
    self.user ||= user
    @updating_user = user
    content_being_saved_by(user)
  end

  alias_method :user, :context_user

  def appointment_group?
    context_type == "AppointmentGroup" || parent_event.try(:context_type) == "AppointmentGroup"
  end

  def appointment_group
    if parent_event.try(:context).is_a?(AppointmentGroup)
      parent_event.context
    elsif context_type == "AppointmentGroup"
      context
    end
  end

  def account
    (context_type == "Account") ? context : nil
  end

  class ReservationError < StandardError; end

  def reserve_for(participant, user, options = {})
    raise ReservationError, "not an appointment" unless context_type == "AppointmentGroup"
    raise ReservationError, "ineligible participant" unless context.eligible_participant?(participant)

    transaction do
      lock! # in case two people two participants try to grab the same slot
      participant.lock! # in case two people try to make a reservation for the same participant

      if options[:cancel_existing]
        context.reservations_for(participant).lock.each do |reservation|
          raise ReservationError, "cannot cancel past reservation" if reservation.end_at < Time.now.utc

          reservation.updating_user = user
          reservation.destroy
        end
      end

      raise ReservationError, "participant has met per-participant limit" if context.max_appointments_per_participant && context.reservations_for(participant).size >= context.max_appointments_per_participant
      raise ReservationError, "all slots filled" if participants_per_appointment && child_events.size >= participants_per_appointment
      raise ReservationError, "participant has already reserved this appointment" if child_events_for(participant).present?

      event = child_events.build
      event.updating_user = user
      event.context = participant
      event.workflow_state = :locked
      event.comments = options[:comments]
      event.save!
      if active?
        self.workflow_state = "locked"
        save!
      end
      context.clear_cached_available_slots!
      event
    end
  end

  def child_events_for(participant)
    if child_events.loaded?
      child_events.select { |e| e.has_asset?(participant) }
    else
      child_events.where(context_type: participant.class.name, context_id: participant)
    end
  end

  def participants_per_appointment
    if override_participants_per_appointment?
      super
    else
      context.is_a?(AppointmentGroup) ? context.participants_per_appointment : nil
    end
  end

  def participants_per_appointment=(limit)
    # if the given limit is the same as the context's limit, we should not override
    if limit == context.participants_per_appointment && override_participants_per_appointment?
      self.override_participants_per_appointment = false
      super(nil)
    else
      super
      self.override_participants_per_appointment = true
    end
  end

  def update_matching_days=(update)
    @update_matching_days = update == "1" || update == true || update == "true"
  end

  def all_day
    super || (new_record? && self.start_at && self.start_at == self.end_at && self.start_at.strftime("%H:%M") == "00:00")
  end

  def to_atom(opts = {})
    extend ApplicationHelper

    title = t(:feed_item_title, "Calendar Event: %{event_title}", event_title: self.title) unless opts[:include_context]
    title = t(:feed_item_title_with_context, "Calendar Event, %{course_or_account_name}: %{event_title}", course_or_account_name: context.name, event_title: self.title) if opts[:include_context]

    {
      title:,
      author: context.name,
      updated: updated_at.utc,
      published: created_at.utc,
      link: "http://#{HostUrl.context_host(context)}/#{context_url_prefix}/calendar?month=#{start_at&.strftime("%m")}&year=#{start_at&.strftime("%Y")}#calendar_event_#{id}",
      id: "tag:#{HostUrl.default_host},#{created_at.strftime("%Y-%m-%d")}:/calendar_events/#{feed_code}_#{start_at&.strftime("%Y-%m-%d-%H-%M") || "none"}_#{end_at&.strftime("%Y-%m-%d-%H-%M")}",
      content: "#{datetime_string(start_at, end_at)}<br/>#{description}"
    }
  end

  def to_ics(in_own_calendar: true, preloaded_attachments: {}, user: nil, user_events: [])
    CalendarEvent::IcalEvent.new(self).to_ics(in_own_calendar:,
                                              preloaded_attachments:,
                                              include_description: true,
                                              user_events:)
  end

  def self.max_visible_calendars
    10
  end

  set_policy do
    given { |user, session| context.grants_right?(user, session, :read) } # students.include?(user) }
    can :read

    given do |user, session|
      if appointment_group?
        context.grants_right?(user, session, :read_appointment_participants)
      else
        !hidden? || context.grants_right?(user, session, :manage_calendar)
      end
    end
    can :read_child_events

    given { |user, session| parent_event && appointment_group? && parent_event.grants_right?(user, session, :manage) }
    can :read and can :delete

    given { |user, session| appointment_group? && context.grants_right?(user, session, :manage) }
    can :manage

    given do |user, session|
      appointment_group? && (
        grants_right?(user, session, :manage) ||
        (context.grants_right?(user, :reserve) && context.participant_for(user).present?)
      )
    end
    can :reserve

    given do |user, session|
      if account
        context.grants_right?(user, session, :manage_account_calendar_events)
      else
        context.grants_right?(user, session, :manage_calendar)
      end
    end
    can :read and can :create

    given do |user, session|
      (!locked? || context.is_a?(AppointmentGroup)) && !deleted? && (
      if account
        context.grants_right?(user, session, :manage_account_calendar_events)
      else
        context.grants_right?(user, session, :manage_calendar)
      end
    )
    end
    can :update and can :update_content

    given do |user, session|
      !deleted? && (
      if account
        context.grants_right?(user, session, :manage_account_calendar_events)
      else
        context.grants_right?(user, session, :manage_calendar)
      end
    )
    end
    can :delete
  end

  class IcalEvent
    include Api
    include Rails.application.routes.url_helpers
    include HtmlTextHelper

    def initialize(event)
      # type: CalendarEvent or AbstractAssignment
      @event = event
    end

    def location; end

    def to_ics(in_own_calendar:, preloaded_attachments: {}, include_description: false, user_events: [])
      cal = Icalendar::Calendar.new
      # to appease Outlook
      cal.append_custom_property("METHOD", "PUBLISH")

      event = Icalendar::Event.new
      event.ip_class = "PUBLIC"

      start_at = @event.is_a?(CalendarEvent) ? @event.start_at : @event.due_at
      end_at = @event.is_a?(CalendarEvent) ? @event.end_at : @event.due_at

      event.dtstart = Icalendar::Values::DateTime.new(start_at.utc.change(sec: 0), "tzid" => "UTC") if start_at
      event.dtend = Icalendar::Values::DateTime.new(end_at.utc.change(sec: 0), "tzid" => "UTC") if end_at

      if @event.all_day && @event.all_day_date
        event.dtstart = Icalendar::Values::Date.new(@event.all_day_date)
        event.dtstart.ical_params = { "VALUE" => ["DATE"] }
        # per rfc5545 3.6.12, DTEND for all day events can omit DTEND
        event.dtend = nil
      end

      event.summary = @event.is_a?(SubAssignment) ? @event.title_with_required_replies : @event.title

      if @event.description && include_description
        html = api_user_content(@event.description, @event.context, nil, preloaded_attachments, location: @event)
        event.description = html_to_text(html)
        event.x_alt_desc = Icalendar::Values::Text.new(html, { "FMTTYPE" => "text/html" })
      end

      loc_string = if @event.is_a?(CalendarEvent)
                     [@event.location_name, @event.location_address].compact_blank.join(", ")
                   else
                     nil
                   end

      if @event.context_type.eql?("AppointmentGroup")
        # We should only enter this block if a user has made an appointment, so
        # there is always at least one element in current_apts
        current_appts = user_events.select { |appointment| @event.id == appointment[:parent_id] }
        if current_appts.any?
          if event.description.nil?
            event.description = current_appts[0][:course_name] + "\n\n"
          else
            event.description.concat("\n\n" + current_appts[0][:course_name] + "\n\n")
          end

          event.description.concat("Participants: ")
          current_appts.each { |appt| event.description.concat("\n" + appt[:user]) }
          comments = current_appts.pluck(:comments).join(",\n")
          event.description.concat("\n\n" + comments)
        end
      end

      event.location = loc_string
      event.dtstamp = Icalendar::Values::DateTime.new(@event.updated_at.utc.change(sec: 0), "tzid" => "UTC") if @event.updated_at

      tag_name = @event.class.name.underscore

      # Covers the case for when personal calendar event is created so that HostUrl finds the correct UR:
      url_context = @event.context
      if url_context.is_a? User
        url_context = url_context.account
      end

      tag_name_and_id = if @event.is_a?(SubAssignment)
                          "assignment_#{@event.parent_assignment_id}"
                        else
                          "#{tag_name}_#{@event.id}"
                        end

      event_context = @event.is_a?(CalendarEvent) ? @event.effective_context : @event.context

      # This will change when there are other things that have calendars...
      # can't call calendar_url or calendar_url_for here, have to do it manually
      event.url =         "https://#{HostUrl.context_host(url_context)}/calendar?include_contexts=#{event_context.asset_string}&month=#{start_at.try(:strftime, "%m")}&year=#{start_at.try(:strftime, "%Y")}##{tag_name_and_id}"
      event.uid =         "event-#{tag_name.tr("_", "-")}-#{@event.id}"
      event.sequence =    0

      if @event.respond_to?(:applied_overrides)
        @event.applied_overrides.try(:each) do |override|
          next unless override.due_at_overridden

          title = @event.is_a?(SubAssignment) ? @event.title_with_required_replies : @event.title
          tag_name = override.class.name.underscore
          event.uid       = "event-#{tag_name.tr("_", "-")}-#{override.id}"
          event.summary   = "#{title} (#{override.title})"
          # TODO: event.url
        end
      end

      # make an effort to find an associated course without diving too deep down the rabbit hole
      associated_course = nil
      if @event.is_a?(CalendarEvent)
        if @event.effective_context.is_a?(Course)
          associated_course = @event.effective_context
        elsif @event.effective_context.respond_to?(:context) && @event.effective_context.context.is_a?(Course)
          associated_course = @event.effective_context.context
        end
      elsif @event.respond_to?(:context) && @event.context_type == "Course"
        associated_course = @event.context
      end

      event.summary += " [#{associated_course.course_code}]" if associated_course

      event = nil unless start_at
      return event unless in_own_calendar

      cal.add_event(event) if event

      cal.to_ical
    end
  end

  def validate_conference_visibility
    return unless web_conference_id_changed?
    return if web_conference_id.nil?

    unless user.blank? || web_conference.grants_right?(user, :read)
      errors.add(:web_conference_id, "cannot add web conference without read access for user")
      return
    end
    conference_context = case context
                         when Course, Group
                           context
                         when CourseSection
                           context.course
                         end
    if conference_context != web_conference.context
      errors.add(:web_conference_id, "cannot add web conference from different context")
    end
  end
end
