class EasyMeeting < ActiveRecord::Base
  include Redmine::SafeAttributes

  # These enums are defined based on iCalendar property. Do not change this.
  # Priority: CUA iCalendar priority property which have three-level scheme
  # Privacy: its only provide an intention of the owner for the access
  enum priority: ['high', 'normal', 'low']
  enum privacy: ['xpublic', 'xprivate', 'confidential']

  MAX_BIG_RECURRING_COUNT = 100

  belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
  belongs_to :project
  belongs_to :easy_room

  scope :between, lambda { |from, to| where(start_time: from..to, end_time: from..to) }

  has_many :easy_invitations, lambda { order(:accepted => :desc) }, :dependent => :destroy
  has_many :users, :through => :easy_invitations, :after_add => :changed_users, :after_remove => :changed_users

  validates :author, :name, :start_time, :end_time, :presence => true
  validates :name, :mails, :place_name, length: { maximum: 255 }
  validates :uid, uniqueness: true
  validate :validate_mails
  validate :validate_room_capacity
  validate :validate_room_conflicts
  validate :validate_user_count
  validate :validate_times
  validate :validate_big_recurring

  validates_inclusion_of :priority, in: EasyMeeting.priorities.keys
  validates_inclusion_of :privacy, in: EasyMeeting.privacies.keys

  attr_protected :id
  attr_accessor :do_not_send_notifications

  after_initialize :set_defaults
  before_create :set_uid
  # after_update :reset_invitations
  before_save :save_big_recurring_state
  before_save :add_author_if_included
  after_save :notify_external_users, :notify_users, if: proc { |easy_meeting|
      !easy_meeting.big_recurring_children? &&
      easy_meeting.do_not_send_notifications != true &&
      (easy_meeting.changed? || @users_changed)
    }
  after_create :accept_author_invitation
  after_commit :update_big_recurring

  include EasyPatch::Acts::Repeatable
  acts_as_easy_repeatable before_save: lambda { |new_one, orig| orig.repeat_before_save(new_one) },
                          start_col: :start_time,
                          end_col: :end_time,
                          repeat_parent_id_col: :easy_repeat_parent_id

  # Big recurring event is created immediately after commit
  scope :easy_to_repeat, -> do
    m_table = EasyMeeting.table_name
    next_start = Date.today + 5.days
    easy_repeating.where("#{m_table}.big_recurring = ?", false).
                   where("#{m_table}.easy_next_start <= ? OR #{m_table}.easy_next_start IS NULL", next_start)
  end


  html_fragment :description, :scrub => :strip

  safe_attributes 'name',
                  'all_day',
                  'start_time',
                  'end_time',
                  'easy_room_id',
                  'project_id',
                  'description',
                  'user_ids',
                  'mails',
                  'include_author',
                  'place_name',
                  'priority',
                  'privacy',
                  'big_recurring',
                  'uid', if: proc {|e| e.new_record? || e.editable? }

  def set_defaults
    return unless new_record?
    next_hour = Time.now.round_min_to(60)
    self.start_time ||= next_hour
    self.end_time ||= next_hour + 1.hour
    self.author ||= User.current
    self.include_author = true
  end

  def repeat_before_save(new_one)
    new_one.user_ids = user_ids
    new_one.uid = EasyUtils::UUID.generate

    if big_recurring?
      new_one.big_recurring = true
    end
  end

  # Method included from `EasyPatch::Acts::Repeatable`. Called after_save.
  # Since new mettings will be created on new Thread this entity must be commited
  # on DB so new callback `update_big_recurring` will be triggered.
  def create_repeated
    if @big_recurring_change_state.nil? || @big_recurring_change_state.empty?
      super
    end
  end

  # Must be on `after_commit` callback
  # Be careful with:
  # - instance variable is also duplicated via `.dup`
  # - this method is also triggered after children is created via big recurring
  def update_big_recurring
    if !easy_repeat_parent_id? &&
       ((@big_recurring_change_state && @big_recurring_change_state.any?) ||
        (@big_recurring_attrs_changed && @big_recurring_attrs_changed.any?))

      EasyCalendar::BigRecurringJob.perform_async(self, @big_recurring_change_state, @big_recurring_attrs_changed)
    end
  end

  # easy_is_repeating?
  #   easy_is_repeating_was?
  #     big_recurring?
  #       big_recurring_was?
  #
  # 0 1 0 1 - Delete all
  # 0 1 1 1 - Delete all
  # 1 0 1 0 - Create all
  # 1 0 1 1 - Create all
  # 1 1 0 1 - Delete all, reset counter
  # 1 1 1 0 - Delete all, create all
  # x x x x - Nothing
  def save_big_recurring_state
    # Only parent can change big recurring
    return if big_recurring_children?

    @big_recurring_change_state = []

    # If recurring setting or times changed
    if (big_recurring? && easy_is_repeating? && !easy_repeat_parent_id?) && (easy_repeat_settings_changed? || start_time_changed? || end_time_changed?)
      @big_recurring_change_state << :reset_counter << :delete_all << :create_all
      return
    end

    state = [
      easy_is_repeating,
      easy_is_repeating_was,
      big_recurring,
      big_recurring_was
    ]

    case state
    when [false, true, false, true], [false, true, true, true]
      @big_recurring_change_state << :delete_all

    when [true, false, true, false], [true, false, true, true]
      @big_recurring_change_state << :create_all

    when [true, true, false, true]
      @big_recurring_change_state << :delete_all << :reset_counter

    when [true, true, true, false]
      @big_recurring_change_state << :delete_all << :create_all

    when [true, true, true, true]
      @big_recurring_change_state << :update_all
      @big_recurring_attrs_changed = changed - ['easy_repeat_settings']
    end
  end

  def big_recurring_children?
    big_recurring? && easy_repeat_parent_id?
  end

  def start_time=(t)
    write_attribute :start_time, parse_date_time_attribute(t)
  end

  def end_time=(t)
    write_attribute :end_time, parse_date_time_attribute(t)
  end

  def duration_hours
    (end_time - start_time) / 1.hour
  end

  def external_mails
    parse_mails(self.mails)
  end

  def include_author
    return @include_author unless @include_author.nil?
    users.include?(self.author)
  end

  def include_author?
    include_author
  end

  def include_author=(val)
    @include_author = val.to_s.to_boolean
  end

  def user_invited?(user)
    # easy_invitations.where(:user_id => user.id).exists?
    !!invitation_for(user)
  end

  def invitation_for(user)
    @invitation_for ||= {}
    @invitation_for[user.id] ||= easy_invitations.detect{ |i| i.user_id == user.id }
    @invitation_for[user.id]
  end

  def accept!(user=User.current)
    accept_or_decline!(user, true)
  end

  def decline!(user=User.current)
    accept_or_decline!(user, false)
  end

  def accept_or_decline!(user, accepted)
    if big_recurring?
      check_id = easy_repeat_parent_id || id
      meeting_ids = EasyMeeting.where("easy_repeat_parent_id = :id OR id = :id", id: check_id).ids
    else
      meeting_ids = [id]
    end

    scope = EasyInvitation.where(easy_meeting_id: meeting_ids)
    scope = scope.where(user_id: user) if user
    scope.update_all(accepted: accepted)
  end

  def accepted_by?(user)
    !!invitation_for(user).try(:accepted?)
  end

  def declined_by?(user)
    !!invitation_for(user).try(:declined?)
  end

  def total_user_count
    total = external_mails.size + user_ids.size
    total += 1 if @include_author && !users.include?(User.current)
    total
  end

  def user_ids=(user_ids)
    user_ids.reject!(&:blank?)
    user_ids.map!(&:to_i)
    user_ids.uniq!

    groups = Principal.from('groups_users').where(groups_users: { group_id: user_ids }).pluck(:user_id, :group_id)
    groups.each do |user_id, group_id|
      user_ids.delete(group_id)
      user_ids << user_id
    end

    if include_author?
      user_ids << author.id
    end

    user_ids.uniq!
    super(user_ids)
  end

  def easy_repeate_update_time_cols(time_vector, start_timepoint = nil, options={})
    # without localtime problems with summer/winter time
    self.start_time = self.start_time.localtime + time_vector
    self.end_time = self.end_time.localtime + time_vector
  end

  def set_uid
    self.uid = EasyUtils::UUID.generate if self.uid.blank?
  end

  def etag
    "#{self.id}-#{self.updated_at.to_i}"
  end

  def destroy_all_repeated
    check_id = easy_repeat_parent_id || id
    EasyMeeting.where("easy_repeat_parent_id = :id OR id = :id", id: check_id).destroy_all
  end

  def editable?(user = nil)
    user ||= User.current

    !big_recurring_children? && (
      user.admin? || (user.id == author_id) || user.allowed_to_globally?(:edit_meetings)
    )
  end

  private

  def validate_mails
    if external_mails.detect{|mail| mail !~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i}
      errors.add(:mails, :invalid_plural)
    end
  end

  def validate_room_capacity
    if easy_room.try(:capacity?)
      if total_user_count > easy_room.capacity
        errors.add(:base, l(:error_room_capacity_breached, user_count: total_user_count, capacity: easy_room.capacity))
      end
    end
  end

  def validate_room_conflicts
    if easy_room && start_time && end_time
      meetings = EasyMeeting.arel_table
      conflict = easy_room.easy_meetings
        .where(
          meetings[:id].not_eq(id).and(
            meetings[:start_time].lt(end_time).and(
              meetings[:end_time].gt(start_time)
            )
          )
        ).first

      if conflict
        errors.add(:base, l(:error_meeting_room_conflict, conflict: conflict.name))
      end
    end
  end

  def validate_user_count
    errors.add(:base, l(:error_any_user_in_meeting)) if !include_author? && users.size == 0
  end

  def validate_times
    if self.end_time && self.start_time && self.end_time < self.start_time
      errors.add :base, l(:error_meeting_end_must_be_greater_than_start, :end_date => format_time(self.end_time), :start_date => format_time(self.start_time))
    end
  end

  def validate_big_recurring
    return if !easy_is_repeating?
    return if !big_recurring?

    case easy_repeat_settings['endtype']
    when 'count'
      count = easy_repeat_settings['endtype_count_x'].to_i
      if count > MAX_BIG_RECURRING_COUNT
        errors.add(:big_recurring, l(:error_easy_meeting_big_recurring_max_count))
      end
    when 'date'
      # During repeating MAX_BIG_RECURRING_COUNT is taken
    else
      errors.add(:big_recurring, l(:error_easy_meeting_big_recurring_endtype_setting))
    end
  end

  # If does not work - try `after_commit` callback.
  # There was a problem with `reload` in callback.
  def notify_users
    mails = EmailAddress.where(notify: true, user_id: self.reload.easy_invitations.joins(:user).where(users: { status: Principal::STATUS_ACTIVE }).where.not(user: self.author).pluck(:user_id)).pluck(:address)
    mails.each do |mail|
      EasyCalendarMailer.easy_meeting_invitation(self, mail, mails).deliver
    end
  end

  # If does not work - try `after_commit` callback.
  # There was a problem with `reload` in callback.
  def notify_external_users
    external_mails.each do |mail|
      EasyCalendarMailer.easy_meeting_invitation(self, mail, nil, :external => true).deliver
    end
  end

  def parse_date_time_attribute(t)
    time = if t.is_a?(Hash) && t['date'] && t['time']
      Time.parse("#{t['date']} #{t['time']}")
    elsif t.is_a?(String)
      t.to_datetime
    end
    time ? User.current.user_civil_time_in_zone(time.year, time.month, time.day, time.hour, time.min, time.sec) : t
  rescue ArgumentError
    nil
  end

  def parse_mails(mail_str)
    if mail_str.present?
      mail_str.split(/,\s*/)
    else
      []
    end
  end

  # Stop spaming
  # def reset_invitations
  #   # accept_or_decline!(nil, nil)
  #   easy_invitations.update_all(accepted: nil)
  # end

  # add author to users if author include himself in meeting
  def add_author_if_included
    if include_author?
      self.user_ids += [self.author.id] unless self.user_ids.include?(self.author.id)
    else
      users = self.user_ids.to_a
      users.delete(self.author_id)
      self.user_ids = users
    end
  end

  # auto accept invitation if author inlude himself in meeting
  def accept_author_invitation
    if include_author?
      accept!(self.author)
    end
  end

  def changed_users(_user)
    @users_changed = true
  end

end
