require 'thin'
require 'mauve/mauve_thread'
require 'digest/sha1'

module Mauve
  # 
  # API to control the web server
  #
  class Pop3Server < MauveThread

    include Singleton

    attr_reader :port, :ip
    
    def initialize
      super
      self.port = 1110
      self.ip = "0.0.0.0"
    end
   
    def port=(pr)
      raise ArgumentError, "port must be an integer between 0 and #{2**16-1}" unless pr.is_a?(Integer) and pr < 2**16 and pr > 0
      @port = pr
    end
    
    def ip=(i)
      raise ArgumentError, "ip must be a string" unless i.is_a?(String)
      #
      # Use ipaddr to sanitize our IP.
      #
      IPAddr.new(i)

      @ip = i
    end

    def logger
      @logger ||= Log4r::Logger.new(self.class.to_s)
    end

    def main_loop
      unless @server and @server.running?
        @server = Mauve::Pop3Backend.new(@ip.to_s, @port)
        logger.info "Listening on #{@server.to_s}"
        #
        # The next statment doesn't return.
        #
        @server.start
      end
    end

    def stop
      if @server.running?
        @server.stop
      else
        @server.stop!
      end

      super
    end

    def join
      @server.stop! if @server

      super
    end

  end    

  class Pop3Backend < Thin::Backends::TcpServer

    def logger
      @logger ||= Log4r::Logger.new(self.class.to_s)
    end
        
    # Connect the server
    def connect
      @signature = EventMachine.start_server(@host, @port, Pop3Connection)
    end
        
    def disconnect
      #
      # Only do this if EventMachine is still going.. The http_server may have
      # stopped it already.
      #
      EventMachine.stop_server(@signature) if EventMachine.reactor_running?
    end

  end


  class Pop3Connection < EventMachine::Connection

    attr_reader :user

    CRLF = "\r\n"

    def logger
      @logger ||= Log4r::Logger.new(self.class.to_s)
    end

    def post_init
      logger.info "New connection"
      send_data "+OK #{self.class.to_s} started"
      @state = :authorization
      @user  = nil
      @messages = []
      @level    = nil
    end

    def permitted_commands(state=@state)
     case @state
        when :authorization
          %w(QUIT USER PASS CAPA)
        when :transaction
          %w(QUIT STAT LIST RETR DELE NOOP RSET UIDL CAPA)
        when :update
          %w(QUIT)
      end
    end

    def capabilities(state=@state)
     case @state
        when :transaction
          %w(CAPA UIDL)
        when :authorization
          %w(CAPA UIDL USER) 
        else
          []
      end
    end

    def receive_data (data)
      data.split(CRLF).each do |cmd|
        break if error?

        if cmd =~ Regexp.new('\A('+self.permitted_commands.join("|")+')\b')
          case $1
            when "QUIT"
              do_process_quit cmd
            when "USER"
              do_process_user cmd
            when "PASS"
              do_process_pass cmd
            when "STAT"
              do_process_stat cmd
            when "LIST"
              do_process_list cmd
            when "RETR"
              do_process_retr cmd
            when "DELE"
              do_process_dele cmd
            when "NOOP"
              do_process_noop cmd
            when "RSET"
              do_process_rset cmd
            when "CAPA"
              do_process_capa cmd
            when "UIDL"
              do_process_uidl cmd
            else
              do_process_error cmd
          end
        else
          do_process_error cmd
        end
      end
    end
  
    def send_data(d)
      d += CRLF
      super unless error?
    end

    def do_process_capa(a)
      send_data (["+OK Capabilities follow:"] + self.capabilities + ["."]).join(CRLF)
    end

    def do_process_user(s)
      allowed_levels = Mauve::AlertGroup::LEVELS.collect{|l| l.to_s}

      if s =~ /\AUSER +(\w+)\+(#{allowed_levels.join("|")})/
        # Allow alerts to be shown by level.
        #
        @user  = $1
        @level = $2
        #
        send_data "+OK Only going to show #{@level} alerts."

     elsif s =~ /\AUSER +([\w]+)/
        @user =  $1

        send_data "+OK"
      else
        send_data "-ERR Username not understood."
      end
    end

    def do_process_pass(s)
      
      if @user and s =~ /\APASS +(\S+)/
        if Mauve::Authentication.authenticate(@user, $1)
          @state = :transaction
          send_data "+OK Welcome #{@user} (#{@level})." 
        else
          send_data "-ERR Authentication failed."
        end        
      else
        send_data "-ERR USER comes first."
      end
    end

    def do_process_error(a)
      send_data "-ERR Unknown comand."
    end

    def do_process_noop(a)
      send_data "+OK Thanks."
    end

    alias do_process_dele do_process_noop

    def do_process_quit(a)
      @state = :update

      send_data "+OK bye."

      close_connection_after_writing
    end

    def do_process_stat(a)
      send_data "+OK #{self.messages.length} #{self.messages.inject(0){|s,m| s+= m[1].length}}"
    end

    def do_process_list(a)
      d = []
      if a =~ /\ALIST +(\d+)\b/
        ind = $1.to_i
        if ind > 0 and ind <= self.messages.length
          d << "+OK #{ind} #{self.messages[ind-1][1].length}"
        else
          d << "-ERR Unknown message."
        end
      else
        d << "+OK #{self.messages.length} messages (#{self.messages.inject(0){|s,m| s+= m[1].length}} octets)."
        self.messages.each_with_index{|m,i| d << "#{i+1} #{m[1].length}"}
        d << "."
      end

      send_data d.join(CRLF)
    end

    def do_process_uidl(a)
      if a =~ /\AUIDL +(\d+)\b/
        ind = $1.to_i
        if ind > 0 and ind <= self.messages.length
          m = self.messages[ind-1][0].id
          send_data "+OK #{ind} #{m}"
        else
          send_data "-ERR Message not found."
        end
      else
        d = ["+OK "]
        self.messages.each_with_index{|m,i| d << "#{i+1} #{m[0].id}"}
        d << "."

        send_data d.join(CRLF)
      end
    end

    def do_process_retr(a)
      if a =~ /\ARETR +(\d+)\b/
        ind = $1.to_i
        if ind > 0 and ind <= self.messages.length
          alert_changed, msg = self.messages[ind-1]
          send_data ["+OK #{msg.length} octets", msg, "."].join(CRLF)
          note =  "#{alert_changed.update_type.capitalize} notification downloaded via POP3 by #{@user}" 
          logger.info note+" about #{alert_changed}."
          h = History.new(:alerts => [alert_changed.alert_id], :type => "notification", :event => note)
          logger.error "Unable to save history due to #{h.errors.inspect}" if !h.save
        else
          send_data "-ERR Message not found."
        end
      else
        send_data "-ERR Boo."
      end

    end

    #
    # These are the messages in the mailbox.
    #
    def messages
      if @messages.empty?
        @messages = []

        email = Configuration.current.notification_methods['email']

        alerts_seen = []

        #
        # A maximum of the 100 most recent alerts.
        #
        AlertChanged.first(100, :person => self.user, :was_relevant => true).each do |a|
          #
          # Not interested in alerts 
          #
          next unless @level.nil? or a.level.to_s == @level

          #
          # Only interested in alerts
          #
          next unless a.alert.is_a?(Mauve::Alert)

          #
          # Only one message per alert.
          #
          next if alerts_seen.include?([a.alert_id, a.update_type])

          relevant = case a.update_type
            when "raised"
              a.alert.raised?
            when "acknowledged"
              a.alert.acknowledged?
            when "cleared"
              a.alert.cleared?
            else
              false
          end

          next unless relevant

          alerts_seen << [a.alert_id, a.update_type]

          @messages << [a, email.prepare_message(self.user+"@"+Server.instance.hostname, a.alert, [])]
        end
      end
      
      @messages
    end

  end

end