
# This is the client model for an entire Agenda.  Class methods refer to
# the agenda as a whole.  Instance methods refer to an individual agenda
# item.
#

class Agenda
  Vue.util.defineReactive @@index, []
  @@etag = nil
  @@digest = nil
  Vue.util.defineReactive @@date, ''
  Vue.util.defineReactive @@approved, '?'
  @@color = 'blank'

  def self.banner
    @@banner
  end

  # (re)-load an agenda, creating instances for each item, and linking
  # each instance to their next and previous items.
  def self.load(list, digest)
    return unless list
    before = @@index
    @@digest = digest
    @@index = []
    prev = nil

    @@banner = list.first.banner

    list.each do |item|
      item = Agenda.new(item)
      item.prev = prev
      prev.next = item if prev
      prev = item
      @@index << item
    end

    # remove president attachments from the normal flow
    @@index.each do |pres|
      match = (pres.title == 'President' and pres.text and pres.text.
        match(/Additionally, please see Attachments (\d) through (\d\d?)\./))
      next unless match

      # find first and last president report; update shepherd along the way
      first = last = nil
      @@index.each do |item|
        first = item if item.attach == match[1]
        item._shepherd ||= pres.shepherd if first and !last
        last  = item if item.attach == match[2]
      end

      # remove president attachments from the normal flow
      if first and last and not Minutes.started
        first.prev.next = last.next
        last.next.prev = first.prev
        last.next.index = first.index
        first.index = nil
        last.next = pres
        first.prev = pres
      end
    end

    @@date = Date.new(@@index[0].timestamp).toISOString()[/(.*?)T/, 1]
    Main.refresh()
    Chat.agenda_change(before, @@index)
    return @@index
  end

  # fetch agenda if etag is not supplied
  def self.fetch(etag, digest)
    if etag
      @@etag = etag
    elsif digest != @@digest or not @@etag
      if PageCache.enabled
        loaded = false

        # if bootstrapping and cache is available, load it
        if not digest
          caches.open('board/agenda').then do |cache|
            cache.match("../#{@@date}.json").then do |response|
              if response
                response.json().then do |json|
                  Agenda.load(json) unless loaded
                  Main.refresh()
                end
              end
            end
          end
        end

        # set fetch options: credentials and etag
        options = {credentials: 'include'}
        options['headers'] = {'If-None-Match' => @@etag} if @@etag
        request = Request.new("../#{@@date}.json", options)

        # perform fetch
        fetch(request).then do |response|
          if response and response.ok
            loaded = true

            # load response into the agenda
            response.clone().json().then do |json|
              @@etag = response.headers.get('etag')
              Agenda.load(json)
              Main.refresh()
            end

            # save response in the cache
            caches.open('board/agenda').then do |cache|
              cache.put(request, response)
            end
          end
        end
      else
        # AJAX fallback
        xhr = XMLHttpRequest.new()
        xhr.open('GET', "../#{@@date}.json", true)
        xhr.setRequestHeader('If-None-Match', @@etag) if @@etag
        xhr.responseType = 'text'
        def xhr.onreadystatechange()
          if xhr.readyState==4 and xhr.status==200 and xhr.responseText!=''
            @@etag = xhr.getResponseHeader('ETag')
            Agenda.load(JSON.parse(xhr.responseText))
            Main.refresh()
          end
        end
        xhr.send()
      end
    end

    @@digest = digest
  end

  # return the entire agenda
  def self.index
    @@index
  end

  # find an agenda item by path name
  def self.find(path)
    result = nil
    path = path.gsub(/\W+/, '-')
    @@index.each do |item|
      result = item if item.href == path
    end
    return result
  end

  # initialize an entry by copying each JSON property to a class instance
  # variable.
  def initialize(entry)
    entry.each_pair do |name, value|
      self["_#{name}"] = value
    end
  end

  # provide read-only access to a number of properties
  attr_reader :attach, :title, :owner, :timestamp, :digest, :mtime
  attr_reader :approved, :roster, :prior_reports, :stats, :people, :notes
  attr_reader :chair_email, :mail_list, :warnings, :flagged_by

  # provide read/write access to other properties
  attr_accessor :index, :shepherd
  attr_writer :color

  def fulltitle
    @fulltitle || @title
  end

  # override missing if minutes aren't present
  def missing
    if @missing
      return true
    elsif @attach =~ /^3\w$/
      if Server.drafts.include? @text[/board_minutes_\w+.txt/]
        return false
      elsif Minutes.get(@title) == 'approved' or @title =~ /^Action/
        return false
      else
        return true
      end
    else
      return false
    end
  end

  # report was marked as NOT accepted during the meeting
  def rejected
    Minutes.rejected and Minutes.rejected.include?(@title)
  end

  # PMC has missed two consecutive months
  def nonresponsive
    @notes and @notes.include? 'missing' and
      @notes.sub(/^.*missing/, '').split(',').length >= 2
  end

  # extract (new) chair name from resolutions
  def chair_name
    if @chair
      @people[@chair].name
    end
  end

  # compute href by taking the title and replacing all non alphanumeric
  # characters with dashes
  def href
    @title.gsub(/[^a-zA-Z0-9]+/, '-')
  end

  # return the text or report for the agenda item
  def text
    @text || @report
  end

  # return comments as an array of individual comments
  def comments
    splitComments(@comments)
  end

  # item's comments excluding comments that have been seen before
  def unseen_comments
    visible = []
    seen = Pending.seen[@attach] || []
    self.comments.each do |comment|
      visible << comment unless seen.include? comment
    end
    return visible
  end

  # retrieve the pending comment (if any) associated with this agenda item
  def pending
    Pending.comments and Pending.comments[@attach]
  end

  # retrieve the action items associated with this agenda item
  def actions
    if @title == 'Action Items'
      @actions
    else
      item = Agenda.find('Action-Items')
      list = []
      if item
        item.actions.each {|action| list << action if action.pmc == @title}
      end
      list
    end
  end

  def special_orders
    items = []

    if @attach =~ /^[A-Z]+$/
      Agenda.index.each do |item|
        items << item if item.attach =~ /^7\w/ and item.roster == @roster
      end
    end

    return items
  end

  def ready_for_review(initials)
    return defined?(@approved) && !self.missing &&
      !@approved.include?(initials) &&
      !(@flagged_by && @flagged_by.include?(initials))
  end

  # determine if this agenda was approved in a later meeting
  def self.approved
    @@approved = 'approved' unless defined? fetch

    if @@approved == '?'
      options = {month: 'long', day: 'numeric', year: 'numeric'}
      date = Date.new(Agenda.file[/\d\d\d\d_\d\d_\d\d/].
        gsub('_', '-') + 'T18:30:00.000Z').toLocaleString('en-US', options)

      Server.agendas.each do |agenda|
        next if agenda <= Agenda.file
        url = "../#{agenda[/\d\d\d\d_\d\d_\d\d/].gsub('_', '-')}.json"
        fetch(url, credentials: 'include').then do |response|
          if response.ok
            response.json().then do |agenda|
              agenda.each do |item|
                @@approved = item.minutes if item.title == date and item.minutes
              end
            end
          end
        end
      end

      @@approved = 'tabled'
    end

    return @@approved
  end

  # the default view to use for the agenda as a whole
  def self.view
    Index
  end

  # buttons to show on the index page
  def self.buttons
    list = [{button: Refresh}]

    if not Minutes.complete
      list << {form: Post, text: 'add item'}
    elsif [:director, :secretary].include? User.role
      list << {form: Summary} unless Minutes.summary_sent
    end

    if User.role == :secretary
      if Agenda.approved == 'approved'
        list << {form: PublishMinutes}
      elsif Minutes.ready_to_post_draft
        list << {form: DraftMinutes}
      end
    end

    list
  end

  # the default banner color to use for the agenda as a whole
  def self.color
    @@color
  end

  def self.color=(color)
    @@color = color
  end

  # fetch the start date
  def self.date
    @@date
  end

  # is today the meeting day?
  def self.meeting_day
    Date.new().toISOString().slice(0,10) >= @@date
  end

  # the default title for the agenda as a whole
  def self.title
    @@date
  end

  # the file associated with this agenda
  def self.file
    "board_agenda_#{@@date.gsub('-', '_')}.txt"
  end

  # get the digest of the file associated with this agenda
  def self.digest
    @@digest
  end

  # previous link for the agenda index page
  def self.prev
    result = {title: 'Help', href: 'help'}

    Server.agendas.each do |agenda|
      date = agenda[/(\d+_\d+_\d+)/, 1].gsub('_', '-')

      if date < @@date and (result.title == 'Help' or date > result.title)
        result = {title: date, href: "../#{date}/"}
      end
    end

    result
  end

  # next link for the agenda index page
  def self.next
    result = {title: 'Help', href: 'help'}

    Server.agendas.each do |agenda|
      date = agenda[/(\d+_\d+_\d+)/, 1].gsub('_', '-')

      if date > @@date and (result.title == 'Help' or date < result.title)
        result = {title: date, href: "../#{date}/"}
      end
    end

    result
  end

  # find the shortest match for shepherd name (example: Rich)
  def self.shepherd
    _shepherd = nil

    firstname = User.firstname.downcase()
    Agenda.index.each do |item|
      if
        item.shepherd and
        firstname.start_with? item.shepherd.downcase() and
        (not _shepherd or item.shepherd.length < _shepherd.length)
      then
        _shepherd = item.shepherd
      end
    end

    return _shepherd
  end

  # summary
  def self.summary
    results = []

    # committee reports
    count = 0
    link = nil
    Agenda.index.each do |item|
      if item.attach =~ /^[A-Z]+$/
        count += 1
        link ||= item.href
      end
    end
    results << {color: 'available', count: count, href: link,
        text: 'committee reports'}

    # special orders
    count = 0
    link = nil
    Agenda.index.each do |item|
      if item.attach =~ /^7[A-Z]+$/
        count += 1
        link ||= item.href
      end
    end
    results << {color: 'available', count: count, href: link,
      text: 'special orders'}

    # discussion items
    count = 0
    link = nil
    Agenda.index.each do |item|
      if item.attach =~ /^8[.A-Z]+$/
        count += 1 unless item.attach == '8.' and not item.text
        link ||= item.href
      end
    end
    results << {color: 'available', count: count, href: link,
      text: 'discussion items'}

    # awaiting preapprovals
    count = 0
    Agenda.index.each do |item|
      count += 1 if item.color == 'ready' and item.title != 'Action Items'
    end
    results << {color: 'ready', count: count, href: 'queue',
      text: 'awaiting preapprovals'}

    # flagged reports
    count = 0
    Agenda.index.each {|item| count += 1 if item.flagged_by}
    results << {color: 'commented', count: count, href: 'flagged',
      text: 'flagged reports'}

    # missing reports
    count = 0
    Agenda.index.each {|item| count += 1 if item.missing}
    results <<  {color: 'missing', count: count, href: 'missing',
      text: 'missing reports'}

    # rejected reports
    count = 0
    Agenda.index.each {|item| count += 1 if item.rejected}
    if Minutes.started or count > 0
      results <<  {color: 'missing', count: count, href: 'rejected',
        text: 'not accepted'}
    end

    return results
  end

  #
  # Methods on individual agenda items
  #

  # default view for an individual agenda item
  def view
    if @title == 'Action Items'
      if @text or Minutes.started
        ActionItems
      else
        SelectActions
      end
    elsif @title == 'Roll Call' and User.role == :secretary
      RollCall
    elsif @title == 'Adjournment' and User.role == :secretary
      Adjournment
    else
      Report
    end
  end

  # buttons and forms to show with this report
  def buttons
    list = []

    unless (@attach !~ /^\d+$/ and @comments === undefined) or Minutes.complete
      # some reports don't have comments
      if self.pending
        list << {form: AddComment, text: 'edit comment'}
      else
        list << {form: AddComment, text: 'add comment'}
      end
    end

    list << {button: Attend} if @title == 'Roll Call'

    if @attach =~ /^(\d+|7?[A-Z]+|4[A-Z]|8[.A-Z])$/
      if User.role == :secretary or not Minutes.complete
        unless Minutes.draft_posted
          if @attach =~ /^8[.A-Z]/
            if @attach =~ /^8[A-Z]/
              list << {form: Post, text: 'edit item'}
            elsif not text or @text.strip().empty?
              list << {form: Post, text: 'post item'}
            else
              list << {form: Post, text: 'edit items'}
            end
          elsif self.missing
            list << {form: Post, text: 'post report'}
          elsif @attach =~ /^7\w/
            list << {form: Post, text: 'edit resolution'}
          else
            list << {form: Post, text: 'edit report'}
          end
        end
      end
    end

    if User.role == :director
      unless self.missing or @comments === undefined or Minutes.complete
        list << {button: Approve} if @attach =~ /^(3[A-Z]|\d+|[A-Z]+)$/
      end

    elsif User.role == :secretary
      unless Minutes.draft_posted
        if @attach =~ /^7\w/
          list << {form: Vote}
        elsif Minutes.get(@title)
          list << {form: AddMinutes, text: 'edit minutes'}
        elsif ['Call to order', 'Adjournment'].include? @title
          list << {button: Timestamp}
        else
          list << {form: AddMinutes, text: 'add minutes'}
        end
      end

      if @attach =~ /^3\w/
        if
          Minutes.get(@title) == 'approved' and
          Server.drafts.include? @text[/board_minutes_\w+\.txt/]
        then
          list << {form: PublishMinutes}
        end
      elsif @title == 'Adjournment'
        if Minutes.ready_to_post_draft
          list << {form: DraftMinutes}
        end
      end
    end

    list
  end

  # determine if this item is flagged, accounting for pending actions
  def flagged
    return true if Pending.flagged and Pending.flagged.include? @attach
    return false unless @flagged_by
    return false if @flagged_by.length == 1 and
      @flagged_by.first == User.initials and
      Pending.unflagged.include?(@attach)
    return ! @flagged_by.empty?
  end

  # determine if this report can be skipped during the course of the meeting
  def skippable
    return false if self.flagged

    if self.missing and Agenda.meeting_day
      return true if @to == 'president'
      return true unless @notes or Server.userid == 'test'
      return false
    end

    return false if @approved and @approved.length < 5 and Agenda.meeting_day
    return true
  end

  # banner color for this agenda item
  def color
    if self.flagged
      'commented'
    elsif @color
      @color
    elsif not @title
      'blank'
    elsif @warnings
      'missing'
    elsif self.missing or self.rejected
      'missing'
    elsif @approved
      if @approved.length < 5
        'ready'
      else
        'reviewed'
      end
    elsif self.title == 'Action Items'
      if self.actions.empty?
        'missing'
      elsif self.actions.any? {|action| action.status.empty?}
        'ready'
      else
        'reviewed'
      end
    elsif @text or @report
      'available'
    elsif @text === undefined
      'missing'
    else
      'reviewed'
    end
  end

  # who to copy on emails
  def cc
    if @to == 'president'
      'operations@apache.org'
    else
      'board@apache.org'
    end
  end
end

Events.subscribe :agenda do |message|
  Agenda.fetch(nil, message.digest) if message.file == Agenda.file
end

Events.subscribe :server do |message|
  Server.drafts  = message.drafts  if message.drafts
  Server.agendas = message.agendas if message.agendas
end
