require 'log4r'
require 'xmpp4r'
require 'xmpp4r/roster'
require 'xmpp4r/muc'
# require 'xmpp4r/xhtml'
# require 'xmpp4r/discovery/helper/helper'
require 'mauve/notifiers/debug'



#
# A couple of monkey patches to fix up all this nonsense.
#
module Jabber
  #
  # Monkey patch of the close commands.  For good reasons, though I can't
  # remember why.
  #
  class Stream
    def close
      #
      # Just close
      #
      close!
    end

    def close!
      10.times do
        pr = 0
        @tbcbmutex.synchronize { pr = @processing }
        break if pr = 0
        Thread::pass if pr > 0
        sleep 1
      end

      # Order Matters here! If this method is called from within 
      # @parser_thread then killing @parser_thread first would 
      # mean the other parts of the method fail to execute. 
      # That would be bad. So kill parser_thread last
      @tbcbmutex.synchronize { @processing = 0 }
      if @fd and !@fd.closed?
        @fd.close 
        stop 
      end
      @status = DISCONNECTED
    end
  end
end




module Mauve
  module Notifiers
 
    #
    # This is the Jabber/XMMP notifiers module.
    #
    module Xmpp

      #
      # The default provider is XMMP, although this should really be broken out
      # into its own provider to allow multple ways of doing XMPP.
      #
      class Default

        include Jabber

        # Atrtribute.
        attr_reader :name

        # Atrtribute.
        attr_accessor :password

        def initialize(name)
          Jabber::logger = self.logger       
#         Jabber::debug = true
#          Jabber::warnings = true

          @name = name
          @mucs = {}
          @roster = nil
          @closing = false
          @client = nil
        end

        # The logger instance
        # 
        # @return [Log4r::Logger]
        def logger
          # Give the logger a sane name
          @logger ||= Log4r::Logger.new self.class.to_s.sub(/::Default$/,"")
        end
       
        # Sets the client's JID
        #
        # @param [String] jid The JID required.
        # @return [Jabber::JID] The client JID.
        def jid=(jid)
          @jid = JID.new(jid)
        end

        # Connects to the XMPP server, and sets up the roster
        #
        # @return [Jabber::Client, NilClass] The connected client, or nil in the case of failure
        def connect
          logger.debug "Starting connection to #{@jid}"

          # Make sure we're disconnected.
          self.close if @client.is_a?(Client)

          @client = Client.new(@jid) 

          @closing = false
          @client.connect
          @client.auth_nonsasl(@password, false)
          @roster = Roster::Helper.new(@client)

          # Unconditionally accept all roster add requests, and respond with a
          # roster add + subscription request of our own if we're not subscribed
          # already
          @roster.add_subscription_request_callback do |ri, presence|
            Thread.new do
              if is_known_contact?(presence.from)
                logger.info("Accepting subscription request from #{presence.from}")
                @roster.accept_subscription(presence.from)
                ensure_roster_and_subscription!(presence.from)
              else
                logger.info("Declining subscription request from #{presence.from}")
                @roster.decline_subscription(presence.from)
              end
            end.join
          end

          @client.add_message_callback do |m|
            receive_message(m)
          end

          @roster.wait_for_roster

          @client.send(Presence.new(nil, "Woo!").set_type(nil))

          logger.info "Connected as #{@jid}"

          # @client.on_exception do |ex, stream, where|
            #
            # The XMPP4R exception clauses in Stream all close the stream, so
            # we just need to reconnect.
            #
          #   unless ex.nil? or @closing 
          #     logger.warn(["Caught",ex.class,ex.to_s,"during XMPP",where].join(" "))
          #     logger.debug ex.backtrace.join("\n")
          #     self.close
          #   end
          # end
        rescue StandardError => ex
          logger.error "Connect failed #{ex.to_s}"
          logger.debug ex.backtrace.join("\n")
          self.close
          @client = nil
        end  

        def stop
          @client.stop
        end
  
        #
        # Closes the XMPP connection, if possible.  Sets @client to nil.
        #
        # @return [NilClass]
        def close
          @closing = true
          if @client 
            if  @client.is_connected?
              @mucs.each do |jid, muc|
                muc[:client].exit("Goodbye!") if muc[:client].active?
              end 
              @client.send(Presence.new(nil, "Goodbye!").set_type(:unavailable))
            end
            @client.close!
          end
          @client = nil
        end

        # Determines if the client is ready.
        #
        # @return [Boolean]
        def ready?
          @client.is_a?(Jabber::Client) and @client.is_connected?
        end
        
        # Attempt to send an alert using XMPP. 
        #
        # @param [String] destination The JID you're sending the alert to. This should be
        #   a bare JID in the case of an individual, or +muc:room@server+ for
        #   chatrooms (XEP0045).
        #
        # @param [Mauve::Alert] alert This is turned into a pretty
        #   message and sent to the destination as a message, if +conditions+
        #   are met.
        #
        # @param [Array] all_alerts Currently ignored.
        #
        # @param [Hash] conditions Conditions that determine if an alert should be sent
        #
        # @option conditions [Array] :if_presence Checks whether the jid in question
        #   has a presence matching one or more of the choices - see
        #   Mauve::Notifiers::Xmpp::Default#check_jid_has_presence for options.
        #
        # @return [Boolean] 
        def send_alert(destination, alert, all_alerts, conditions = {})
          destination_jid = JID.new(destination)         
 
          was_suppressed = conditions[:was_suppressed] || false
          will_suppress  = conditions[:will_suppress]  || false
          
          if conditions && !check_alert_conditions(destination_jid, conditions) 
            logger.info("Alert conditions not met, not sending XMPP alert to #{destination_jid}")
            return false
          end
         
          template_file = File.join(File.dirname(__FILE__),"templates","xmpp.txt.erb")

          txt = if File.exists?(template_file) 
            ERB.new(File.read(template_file)).result(binding).chomp
          else
            logger.error("Could not find xmpp.txt.erb template")
            alert.to_s
          end

          template_file = File.join(File.dirname(__FILE__),"templates","xmpp.html.erb")

          xhtml = if File.exists?(template_file)
            ERB.new(File.read(template_file)).result(binding).chomp
          else
            logger.error("Could not find xmpp.txt.erb template")
            alert.to_s
          end

          msg_type = (is_muc?(destination_jid) ? :groupchat : :chat)

          send_message(destination_jid, txt, xhtml, msg_type)
        end

        # Sends a message to the destionation.
        def send_message(jid, msg, html_msg=nil, msg_type=:chat)
          return false unless self.ready?

          jid = JID.new(jid) unless jid.is_a?(JID)

          message = Message.new(jid)
          message.body = msg
          if html_msg 
            begin
              html_msg = REXML::Document.new(html_msg) unless html_msg.is_a?(REXML::Document)
              message.add_element(html_msg) 
            rescue REXML::ParseException
              logger.error "Bad XHTML: #{html_msg.inspect}"
            end
          end

          message.to   = jid
          message.type = msg_type

          if message.type == :groupchat and is_muc?(jid)
            jid = join_muc(jid.strip)
            muc = @mucs[jid][:client]

            if muc
              muc.send(message)
              true
            else
              logger.warn "Failed to join MUC #{jid} when trying to send a message"
              false
            end
          else
            #
            # We aren't interested in sending things to people who aren't online.
            #
            ensure_roster_and_subscription!(jid)

            if check_jid_has_presence(jid)
              @client.send(message)
              true
            else
              false
            end
          end
        end

        #
        # Joins a chat, and returns the stripped JID of the chat joined.
        #
        def join_muc(jid, password=nil)
          self.connect unless self.ready?

          return unless self.ready?
 
          if jid.is_a?(String) and jid =~ /^muc:(.*)/
            jid = JID.new($1) 
          end
            
          unless jid.is_a?(JID)
            logger.warn "#{jid} is not a MUC"
            return
          end

          jid.resource = @client.jid.resource if jid.resource.to_s.empty?

          if !@mucs[jid.strip]

            logger.debug("Adding new MUC client for #{jid}")
            
            @mucs[jid.strip] = {:jid => jid, :password => password, :client => Jabber::MUC::MUCClient.new(@client)}
            
            # Add some callbacks
            @mucs[jid.strip][:client].add_message_callback do |m|
              receive_message(m)
            end

            @mucs[jid.strip][:client].add_private_message_callback do |m|
              receive_message(m)
            end

          end

          if !@mucs[jid.strip][:client].active?
            #
            # Make sure we have a resource.
            #
            @mucs[jid.strip][:client].join(jid, password)

            logger.info("Joined #{jid.strip}")
          else
            logger.debug("Already joined #{jid.strip}.")
          end

          #
          # Return the JID object
          #
          jid.strip
        end 
        
        # 
        # Checks whether the destination JID is a MUC. 
        #
        def is_muc?(jid)
          (jid.is_a?(JID)    and @mucs.keys.include?(jid.strip)) or
          (jid.to_s =~ /^muc:(.*)/)

          #
          # It would be nice to use service discovery to determin this, but it
          # turns out that it is shite in xmpp4r.  It doesn't return straight
          # away with an answer, making it a bit useless.  Some sort of weird
          # threading issue, I think.
          #
          # begin
          #   logger.warn caller.join("\n")
          #   cl  = Discovery::Helper.new(@client)
          #   res = cl.get_info_for(jid.strip)
          #   @client.wait
          #   logger.warn "hello #{res.inspect}"
          #   res.is_a?(Discovery::IqQueryDiscoInfo) and res.identity.category == :conference
          # rescue Jabber::ServerError => ex
          #  false
          # end
        end

        # 
        # Checks to see if the JID is in our roster, and whether we are
        # subscribed to it or not. Will add to the roster and subscribe as
        # is necessary to ensure both are true.
        #
        def ensure_roster_and_subscription!(jid)
          self.connect unless self.ready?

          return unless self.ready?

          return jid if is_muc?(jid)

          jid = JID.new(jid) unless jid.is_a?(JID)

          ri = @roster.find(jid).values.first
          @roster.add(jid, nil, true) if ri.nil? 

          ri = @roster.find(jid).values.first
          ri.subscribe unless [:to, :both, :remove].include?(ri.subscription)
          ri.jid
        rescue StandardError => ex
          logger.error("Problem ensuring that #{jid} is subscribed and in mauve's roster: #{ex.inspect}")
          nil
        end

        protected

        def receive_message(msg)
          #
          # Don't talk to self
          #
          if @jid == msg.from or @mucs.any?{|jid, muc| muc.is_a?(Hash) and muc.has_key?(:client) and muc[:client].jid == msg.from}
            return nil
          end 

          # We only want to hear messages from known contacts.
          unless is_known_contact?(msg.from)
            # ignore message
            logger.info "Ignoring message from unknown contact #{msg.from}"
            return nil
          end

          case msg.type
            when :error
              receive_error_message(msg)
            when :groupchat
              receive_groupchat_message(msg)
            else
              receive_normal_message(msg)
          end
        end

        def receive_error_message(msg)
          logger.warn("Caught XMPP error #{msg}") 
          nil
        end

        def receive_normal_message(msg)
          #
          # Treat invites specially
          #
          if msg.x("jabber:x:conference")
            #
            # recieved an invite.  Need to mangle the jid.
            #
            jid =JID.new(msg.x("jabber:x:conference").attribute("jid"))
            # jid.resource = @client.jid.resource
            logger.info "Received an invite to #{jid}"
            unless join_muc(jid)
              logger.warn "Failed to join MUC #{jid} following invitation"
              return nil
            end            
          elsif msg.body
            #
            # Received a message with a body.
            #
            jid = msg.from
          end

          if jid 
            reply = parse_command(msg)
            send_message(jid, reply, nil, msg.type)
          end
        end

        def receive_groupchat_message(msg)
          #
          # We only want group chat messages from MUCs we're already joined to,
          # that we've not sent ourselves, that are not historical, and that
          # match our resource or node in the body.
          #
          if @mucs[msg.from.strip][:client].is_a?(MUC::MUCClient) and
                msg.x("jabber:x:delay") == nil and 
                (msg.body =~ /\b#{Regexp.escape(@mucs[msg.from.strip][:client].jid.resource)}\b/i or
                msg.body =~ /\b#{Regexp.escape(@client.jid.node)}\b/i)

            receive_normal_message(msg) 
          end
        end

        def parse_command(msg)
          case msg.body
            when /help(\s+\w+)?/i
              do_parse_help(msg)
            when /show\s?/i
              do_parse_show(msg)
            when /ack/i
              do_parse_ack(msg)
            when /clear/i
              do_parse_clear(msg)
            when /destroy\s?/i
              "Sorry -- destroy has been disabled.  Try \"clear\" instead."
            else
              File.executable?('/usr/games/fortune') ? `/usr/games/fortune -s -n 60`.chomp : "I'd love to stay and chat, but I'm really quite busy"
          end          
        end

        def do_parse_help(msg)
          msg.body =~ /help\s+(\w+)/i
          cmd = $1
          
          return case cmd
            when /^show/
               <<EOF
Show command: Lists all raised or acknowledged alerts, or the first or last few.

e.g.
  show  -- shows all raised alerts
  show ack -- shows all acknowledged alerts
  show first 10 acknowledged -- shows first 10 acknowledged
  show last 5 raised -- shows last 5 raised alerts
EOF
            when /^ack/
              <<EOF
Acknowledge command: Acknowledges one or more alerts for a set period of time.

The syntax is

  acknowledge <alert list> for <time period> because <note>

 * The alert list is a comma separated list.
 * The time period can be spefied in terms of days, hours, minutes, seconds,
    which can be wall-clock (default), working, or daytime (see the examples).
 * The note is appended to the acknowledgement.

e.g.
  acknowledge 1 for 2 hours -- acknowledges alert no. 1 for 2 wall-clock hours
  ack 1,2,3 for 2 working hours -- acknowledges alerts 1, 2, and 3 for 2 working hours
  ack 4 for 3 days because something bad happened -- acknowledge alert 4 for 3 wall-clock days with the note "something bad happened"
EOF
            when /^destroy/
              <<EOF
Destroy command: Destroys one or more alerts.

The syntax is

  destroy <alert list>

where <alert list> is a comma separated list of alert IDs.

e.g.
  destroy 1,2,3  -- destroys alerts 1, 2, and 3.
EOF

            else
              "I am Mauve #{Mauve::VERSION}.  I understand \"help\", \"show\", \"acknowledge\", and \"destroy\" commands.  Try \"help show\"."
          end       
        end

        def do_parse_show(msg)
          return "Sorry -- I don't understand your show command." unless
             msg.body =~ /show(?:\s+(first|last)\s+(\d+))?(?:\s+(events|raised|ack(?:d|nowledged)?))?/i

          first_or_last = $1
          n_items = ($2 || -1).to_i

          type = $3 || "raised"
          type = "acknowledged" if type =~ /^ack/
          
          msg = []
          
          items = case type
            when "acknowledged"
              Alert.all_acknowledged.all(:order => [:acknowledged_at.asc])
            when "events"
              History.all(:created_at.gte => Time.now - 24.hours)
            else 
              Alert.all_unacknowledged.all(:order => [:raised_at.asc])
          end

          if first_or_last == "first"
            items = items.first(n_items) if n_items >= 0 
          elsif first_or_last == "last"
            items = items.last(n_items) if n_items >= 0 
          end

          return "Nothing to show" if items.length == 0          

          template_file = File.join(File.dirname(__FILE__),"templates","xmpp.txt.erb")
          if File.exists?(template_file)
            template = File.read(template_file)
          else
            logger.error("Could not find xmpp.txt.erb template")
            template = nil
          end 

          (["Alerts #{type}:"] + items.collect do |alert| 
            ERB.new(template).result(binding).chomp
          end).join("\n")
        end

        def do_parse_ack(msg)
          return "Sorry -- I don't understand your acknowledge command." unless
             msg.body =~ /ack(?:nowledge)?\s+([\d\D]+)\s+for\s+(\d+(?:\.\d+)?)\s+(work(?:ing)?|day(?:time)?|wall(?:-?clock)?)?\s*(day|hour|min(?:ute)?|sec(?:ond))s?(?:\s+(?:cos|cause|as|because)?\s*(.*))?/i
          
          alerts, n_hours, type_hours, dhms, note = [$1,$2, $3, $4, $5]

          alerts = alerts.split(/\D+/)

          n_hours = case dhms
            when /^day/
              n_hours.to_f * 24.0
            when /^min/
              n_hours.to_f / 60.0
            when /^sec/
              n_hours.to_f / 3600.0
            else
              n_hours.to_f
          end

          type_hours = case type_hours
            when /^day/
              "daytime"
            when /^work/
              "working"
            else
              "wallclock"
          end

          begin
            now = Time.now
            ack_until = now.in_x_hours(n_hours, type_hours)
          rescue RangeError 
            return "I'm sorry, you tried to acknowedge for far too long, and my buffers overflowed!"
          end

          username = get_username_for(msg.from)

          if is_muc?(Configuration.current.people[username].xmpp)
            return "I'm sorry -- if you want to acknowledge alerts, please do it from a private chat"
          end

          msg = []
          msg << "Acknowledgement results:" if alerts.length > 1

          succeeded = []

          alerts.each do |alert_id|
            alert = Alert.get(alert_id)

            if alert.nil?
              msg << "#{alert_id}: alert not found" 
              next
            end

            if alert.cleared?
              msg << "#{alert_id}: alert already cleared" if alert.cleared?
              next
            end

            if alert.acknowledge!(Configuration.current.people[username], ack_until)
              msg << "#{alert_id}: Acknowledged until #{alert.will_unacknowledge_at.to_s_human}"
              succeeded << alert
            else
              msg << "#{alert_id}: Acknowledgement failed."
            end
          end
  
          #
          # Add the note.
          #
          unless note.to_s.empty?
            note = Alert.remove_html(note)
            h = History.new(:alerts => succeeded, :type => "note", :event => note.to_s, :user => username)
            logger.debug h.errors unless h.save
          end

          return msg.join("\n")
        end

        def do_parse_clear(msg)
          return "Sorry -- I don't understand your clear command." unless
             msg.body =~ /clear\s+([\d\D]+)(?:\s+(?:coz|cause|cos|because|as)?\s*(.*))?/i

          alerts = $1.split(/\D+/)
          note   = $2
          
          username = get_username_for(msg.from)

          if is_muc?(Configuration.current.people[username].xmpp)
            return "I'm sorry -- if you want to clear alerts, please do it from a private chat"
          end

          msg = []
          msg << "Clearing results:" if alerts.length > 1

          alerts.each do |alert_id|
            alert = Alert.get(alert_id)

            if alert.nil?
              msg << "#{alert_id}: alert not found."
              next
            end

            if alert.cleared?
              msg << "#{alert_id}: alert already cleared."
              next
            end 

            if alert.clear!
              msg << "#{alert.to_s} cleared."
            else
              msg << "#{alert.to_s}: clearing failed."
            end
          end
          
          #
          # Add the note.
          #
          unless note.to_s.empty?
            note = Alert.remove_html(note)
            h = History.new(:alerts => succeeded, :type => "note", :event => note.to_s, :user => username)
            logger.debug h.errors unless h.save
          end

          return msg.join("\n")
        end

        def check_alert_conditions(destination, conditions)
          any_failed = conditions.keys.collect do |key|
            case key
              when :if_presence : check_jid_has_presence(destination, conditions[:if_presence])
              else 
                #raise ArgumentError.new("Unknown alert condition, #{key} => #{conditions[key]}")
                # FIXME - clean up this use of :conditions to pass arbitrary
                # parameters to notifiers; for now we need to ignore this. 
                true
            end
          end.include?(false)
          !any_failed
        end
        
        # Checks our roster to see whether the jid has a resource with at least 
        # one of the included presences. Acceptable +presence+ types and their 
        # meanings for individuals:
        #
        #   :online, :offline               - user is logged in or out
        #   :available                      - jabber status is nil (available) or chat
        #   :unavailable -                  - jabber status is away, dnd or xa
        #   :unknown                        - don't know (not in roster)
        #
        # Returns true if at least one of the presence specifiers for the jid
        # is met, false otherwise. Note that if the alerter can't see the alertee's
        # presence, only 'unknown' will match - generally, you'll want [:online, :unknown]
        def check_jid_has_presence(jid, presence_or_presences = [:online, :unknown])
          return true if is_muc?(jid)

          jid = JID.new(jid) unless jid.is_a?(JID)

          self.connect unless self.ready?

          return false unless self.ready?

          presences = [presence_or_presences].flatten
          roster_item = @roster.find(jid)
          roster_item = roster_item[roster_item.keys[0]]
          resource_presences = []
          roster_item.each_presence {|p| resource_presences << p.show } if roster_item

          results = presences.collect do |need_presence|
            case need_presence
              when :online      : (roster_item && [:to, :both].include?(roster_item.subscription) && roster_item.online?)
              when :offline     : (roster_item && [:to, :both].include?(roster_item.subscription) && !roster_item.online?)
              when :available   : (roster_item && [:to, :both].include?(roster_item.subscription) && (resource_presences.include?(nil) ||
                                                                                                      resource_presences.include?(:chat)))
              # No resources are nil or chat
              when :unavailable : (roster_item && [:to, :both].include?(roster_item.subscription) && (resource_presences - [:away, :dnd, :xa]).empty?)
              # Not in roster or don't know subscription
              when :unknown     : (roster_item.nil? || [:none, :from].include?(roster_item.subscription)) 
            else
              raise ArgumentError.new("Unknown presence possibility: #{need_presence}")
            end
          end
          results.include?(true)
        end

        #
        # Returns the username of the jid, if any
        #
        def get_username_for(jid)
          jid = JID.new(jid) unless jid.is_a?(JID)
       
          #
          # Resolve MUC JIDs.
          #
          if is_muc?(jid)
            muc_jid = get_jid_from_muc_jid(jid)
            jid = muc_jid unless muc_jid.nil?
          end
 
          ans = Configuration.current.people.find do |username, person|
            next unless person.xmpp.is_a?(JID)
            person.xmpp.strip == jid.strip
          end

          ans.nil? ? ans : ans.first
        end

        #
        # Tries to establish a real JID from a MUC JID.
        #
        def get_jid_from_muc_jid(jid)
          #
          # Resolve the JID for MUCs.
          #
          jid = JID.new(jid) unless jid.is_a?(JID)
          return nil unless @mucs.has_key?(jid.strip)
          return nil unless @mucs[jid.strip].has_key?(:client)
          return nil unless @mucs[jid.strip][:client].active?

          roster = @mucs[jid.strip][:client].roster[jid.resource]
          return nil unless roster

          x = roster.x('http://jabber.org/protocol/muc#user')
          return nil unless x

          items = x.items
          return nil if items.nil? or items.empty?

          jids = items.collect{|item| item.jid}
          return nil if jids.empty?

          jids.first
        end

        def is_known_contact?(jid)
          !get_username_for(jid).nil?
        end
        
      end
    end
  end
end