# frozen_string_literal: true

# == Schema Information
#
# Table name: events
#
#  id              :integer          not null, primary key
#  created_by_name :string
#  description     :text
#  end_time        :datetime
#  locked          :boolean          default(FALSE)
#  rrule           :string
#  start_time      :datetime
#  title           :string
#  url             :string
#  venue_details   :text
#  created_at      :datetime
#  updated_at      :datetime
#  created_by_id   :integer
#  duplicate_of_id :integer
#  source_id       :integer
#  venue_id        :integer
#
# Indexes
#
#  index_events_on_created_by_id  (created_by_id)
#
# Foreign Keys
#
#  created_by_id  (created_by_id => calagator_users.id)
#

require "calagator/denylist_validator"
require "calagator/duplicate_checking"
require "calagator/decode_html_entities_hack"
require "calagator/strip_whitespace"
require "calagator/url_prefixer"
require "paper_trail"
require "loofah-activerecord"
require "loofah/activerecord/xss_foliate"
require "active_model/sequential_validator"
require "active_model/serializers/xml"

# == Event
#
# A model representing a calendar event.

module Calagator
  class Event < Calagator::ApplicationRecord
    self.table_name = "events"
    self.belongs_to_required_by_default = false

    has_paper_trail
    acts_as_taggable_on :tags

    # xss_foliate strip: %i[title description venue_details]

    include DecodeHtmlEntitiesHack
    include ActiveModel::Serializers::Xml

    # Associations
    belongs_to :created_by, class_name: "Calagator::User", optional: true
    belongs_to :venue, counter_cache: true, optional: true
    belongs_to :source, optional: true

    # Validations
    validates :title, :description, :url, denylist: true
    validates :start_time, :end_time, sequential: true
    validates :title, :start_time, presence: true
    validates :url,
      format: {with: %r{\Ahttps?://(\w+:?\w*@)?(\S+)(:[0-9]+)?(/|/([\w#!:.?+=&%@\-/]))?\Z},
               allow_blank: true}

    before_save :set_created_by_name, if: :created_by_id?
    before_destroy :check_if_locked_before_destroy # prevent locked events from being destroyed

    # Duplicates
    include DuplicateChecking
    duplicate_checking_ignores_attributes :source_id, :version, :venue_id, :tag_list
    duplicate_squashing_ignores_associations :tags, :base_tags, :taggings
    duplicate_finding_scope -> { future.order(:id) }
    after_squashing_duplicates ->(primary) { primary.venue.try(:update_events_count!) }

    # Named scopes
    scope :after_date, lambda { |date|
      where(["start_time >= ?", date]).order(:start_time)
    }
    scope :on_or_after_date, lambda { |date|
      time = date.beginning_of_day
      where("(events.start_time >= :time) OR (events.end_time IS NOT NULL AND events.end_time > :time)",
        time: time).order(:start_time)
    }
    scope :before_date, lambda { |date|
      time = date.beginning_of_day
      where("start_time < :time", time: time).order(start_time: :desc)
    }
    scope :future, -> { on_or_after_date(Time.zone.today) }
    scope :past, -> { before_date(Time.zone.today) }
    scope :within_dates, lambda { |start_date, end_date|
      end_date += 1.day if start_date == end_date
      on_or_after_date(start_date).before_date(end_date)
    }
    scope :find_duplicate, ->(title, start_time, end_time, venue) {
      includes(:venue).where(title: title, start_time: start_time, end_time: end_time, venue: venue)
    }
    # Expand the simple sort order names from the URL into more intelligent SQL order strings
    scope :ordered_by_ui_field, lambda { |ui_field|
      scope = case ui_field
      when "name"
        order(Arel.sql("lower(events.title)"))
      when "venue"
        includes(:venue).order(Arel.sql("lower(venues.title)")).references(:venues)
      when "created_at"
        return order(created_at: :desc)
      else
        all
      end
      scope.order("start_time")
    }

    #---[ Overrides ]-------------------------------------------------------

    def url=(value)
      super(UrlPrefixer.prefix(value))
    end

    # Set the start_time to the given +value+, which could be a Time, Date,
    # DateTime, String, Array of Strings, or nil.
    def start_time=(value)
      super(time_for(value))
    rescue ArgumentError
      errors.add :start_time, "is invalid"
      super(nil)
    end

    # Set the end_time to the given +value+, which could be a Time, Date,
    # DateTime, String, Array of Strings, or nil.
    def end_time=(value)
      super(time_for(value))
    rescue ArgumentError
      errors.add :end_time, "is invalid"
      super(nil)
    end

    def time_for(value)
      value = value.to_s if value.is_a?(Date)
      if value.is_a?(String)
        value = Time.zone.parse(value)
      end # this will throw ArgumentError if invalid
      value
    end
    private :time_for

    #---[ Lock toggling ]---------------------------------------------------

    def lock_editing!
      update_attribute(:locked, true)
    end

    def unlock_editing!
      update_attribute(:locked, false)
    end

    def check_if_locked_before_destroy
      return if !locked
      errors.add :base, "Event must be unlocked before destroying"
      throw(:abort)
    end

    #---[ Searching ]-------------------------------------------------------

    def self.search_tag(tag, opts = {})
      includes(:venue).tagged_with(tag).ordered_by_ui_field(opts[:order])
    end

    def self.search(query, opts = {})
      SearchEngine.search(query, opts)
    end

    #---[ Date related ]----------------------------------------------------

    def current?
      (end_time || start_time) >= Date.today.to_time
    end

    def old?
      (end_time || start_time + 1.hour) <= Time.zone.now.beginning_of_day
    end

    def ongoing?
      return unless end_time && start_time

      (start_time..end_time).cover?(Date.today.to_time)
    end

    def duration
      return 0 unless end_time && start_time

      (end_time - start_time)
    end

    def set_created_by_name
      self.created_by_name = created_by.name_with_email
    end
    private :set_created_by_name
  end
end
