# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/

class Calendar < ApplicationModel
  include ChecksClientNotification
  include CanUniqName

  store :business_hours
  store :public_holidays

  before_create  :validate_public_holidays, :fetch_ical
  before_update  :validate_public_holidays, :fetch_ical
  after_create   :sync_default, :min_one_check
  after_update   :sync_default, :min_one_check
  after_destroy  :min_one_check

=begin

set inital default calendar

  calendar = Calendar.init_setup

returns calendar object

=end

  def self.init_setup(ip = nil)

    # ignore client ip if not public ip
    if ip && ip =~ /^(::1|127\.|10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[0-1]\.|192\.168\.)/
      ip = nil
    end

    # prevent multible setups for same ip
    cache = Cache.get('Calendar.init_setup.done')
    return if cache && cache[:ip] == ip

    Cache.write('Calendar.init_setup.done', { ip: ip }, { expires_in: 1.hour })

    # call for calendar suggestion
    calendar_details = Service::GeoCalendar.location(ip)
    return if !calendar_details

    calendar_details['name'] = Calendar.generate_uniq_name(calendar_details['name'])
    calendar_details['default'] = true
    calendar_details['created_by_id'] = 1
    calendar_details['updated_by_id'] = 1

    # find if auto generated calendar exists
    calendar = Calendar.find_by(default: true, updated_by_id: 1, created_by_id: 1)
    if calendar
      calendar.update!(calendar_details)
      return calendar
    end
    create(calendar_details)
  end

=begin

get default calendar

  calendar = Calendar.default

returns calendar object

=end

  def self.default
    find_by(default: true)
  end

=begin

returns preset of ical feeds

  feeds = Calendar.ical_feeds

returns

  {
    'http://www.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics' => 'US',
    ...
  }

=end

  def self.ical_feeds
    data = YAML.load_file(Rails.root.join('config', 'holiday_calendars.yml'))
    url  = data['url']

    data['countries'].map do |country, domain|
      [format(url, domain: domain), country]
    end.to_h
  end

=begin

get list of available timezones and UTC offsets

  list = Calendar.timezones

returns

  {
    'America/Los_Angeles' => -7
    ...
  }

=end

  def self.timezones
    list = {}
    TZInfo::Timezone.all_country_zone_identifiers.each do |timezone|
      t = TZInfo::Timezone.get(timezone)
      diff = t.current_period.utc_total_offset / 60 / 60
      list[ timezone ] = diff
    end
    list
  end

=begin

syn all calendars with ical feeds

  success = Calendar.sync

returns

  true # or false

=end

  def self.sync
    Calendar.find_each(&:sync)
    true
  end

=begin

syn one calendars with ical feed

  calendar = Calendar.find(4711)
  success = calendar.sync

returns

  true # or false

=end

  def sync(without_save = nil)
    return if !ical_url

    # only sync every 5 days
    if id
      cache_key = "CalendarIcal::#{id}"
      cache = Cache.get(cache_key)
      return if !last_log && cache && cache[:ical_url] == ical_url
    end

    begin
      events = {}
      if ical_url.present?
        events = Calendar.fetch_parse(ical_url)
      end

      # sync with public_holidays
      if !public_holidays
        self.public_holidays = {}
      end

      # remove old ical entries if feed has changed
      public_holidays.each do |day, meta|
        next if !public_holidays[day]['feed']
        next if meta['feed'] == Digest::MD5.hexdigest(ical_url)

        public_holidays.delete(day)
      end

      # sync new ical feed dates
      events.each do |day, summary|
        if !public_holidays[day]
          public_holidays[day] = {}
        end

        # ignore if already added or changed
        next if public_holidays[day].key?('active')

        # create new entry
        public_holidays[day] = {
          active:  true,
          summary: summary,
          feed:    Digest::MD5.hexdigest(ical_url)
        }
      end
      self.last_log = nil
      if id
        Cache.write(
          cache_key,
          { public_holidays: public_holidays, ical_url: ical_url },
          { expires_in: 1.day },
        )
      end
    rescue => e
      self.last_log = e.inspect
    end

    self.last_sync = Time.zone.now
    if !without_save
      save
    end
    true
  end

  def self.fetch_parse(location)
    if location.match?(/^http/i)
      result = UserAgent.get(location)
      if !result.success?
        raise result.error
      end

      cal_file = result.body
    else
      cal_file = File.open(location)
    end

    cals = Icalendar::Calendar.parse(cal_file)
    cal = cals.first
    events = {}
    cal.events.each do |event|
      if event.rrule

        # loop till days
        interval_frame_start = Date.parse("#{Time.zone.now - 1.year}-01-01")
        interval_frame_end   = Date.parse("#{Time.zone.now + 3.years}-12-31")
        occurrences          = event.occurrences_between(interval_frame_start, interval_frame_end)
        if occurrences.present?
          occurrences.each do |occurrence|
            result = Calendar.day_and_comment_by_event(event, occurrence.start_time)
            next if !result

            events[result[0]] = result[1]
          end
        end
      end
      next if event.dtstart < Time.zone.now - 1.year
      next if event.dtstart > Time.zone.now + 3.years

      result = Calendar.day_and_comment_by_event(event, event.dtstart)
      next if !result

      events[result[0]] = result[1]
    end
    events.sort.to_h
  end

  # get day and comment by event
  def self.day_and_comment_by_event(event, start_time)
    day = "#{start_time.year}-#{format('%02d', start_time.month)}-#{format('%02d', start_time.day)}"
    comment = event.summary || event.description
    comment = comment.to_utf8(fallback: :read_as_sanitized_binary)

    # ignore daylight saving time entries
    return if comment.match?(/(daylight saving|sommerzeit|summertime)/i)

    [day, comment]
  end

  private

  # if changed calendar is default, set all others default to false
  def sync_default
    return true if !default

    Calendar.find_each do |calendar|
      next if calendar.id == id
      next if !calendar.default

      calendar.default = false
      calendar.save
    end
    true
  end

  # check if min one is set to default true
  def min_one_check
    if !Calendar.find_by(default: true)
      first = Calendar.order(:created_at, :id).limit(1).first
      return true if !first

      first.default = true
      first.save
    end

    # check if sla's are refer to an existing calendar
    default_calendar = Calendar.find_by(default: true)
    Sla.find_each do |sla|
      if !sla.calendar_id
        sla.calendar_id = default_calendar.id
        sla.save!
        next
      end
      if !Calendar.find_by(id: sla.calendar_id)
        sla.calendar_id = default_calendar.id
        sla.save!
      end
    end
    true
  end

  # fetch ical feed
  def fetch_ical
    sync(true)
    true
  end

  # validate format of public holidays
  def validate_public_holidays

    # fillup feed info
    before = public_holidays_was
    public_holidays.each do |day, meta|
      if before && before[day] && before[day]['feed']
        meta['feed'] = before[day]['feed']
      end
      meta['active'] = if meta['active']
                         true
                       else
                         false
                       end
    end
    true
  end
end
