require 'custodian/util/bytemark'
require 'custodian/util/dns'

require 'digest/sha1'


#
#  This class encapsulates the raising and clearing of alerts via Mauve.
#
#  There is a helper method to update any alerts with details of whether the
# affected host is inside/outside the Bytemark network.
#
#  This is almost Bytemark-specific, although the server it talks to is
# indeed Open Source:
#
#    https://projects.bytemark.co.uk/projects/mauvealert
#
#
module Custodian

  module Alerter

    class AlertMauve < AlertFactory


      #
      # The test this alerter cares about
      #
      attr_reader :test

      #
      # Was this class loaded correctly?
      #
      attr_reader :loaded




      #
      # Constructor
      #
      def initialize( obj )
        @test = obj

        begin
          require 'mauve/sender'
          require 'mauve/proto'
          @loaded = true
        rescue
          puts "ERROR Loading mauve libraries!"
          @loaded = false
        end
      end




      #
      # Generate an alert-message which will be raised via mauve.
      #
      def raise()

        return unless( @loaded )

        #
        # Get ready to send to mauve.
        #
        update         = Mauve::Proto::AlertUpdate.new
        update.alert   = []
        update.source  = @settings.alert_source()
        update.replace = false

        #
        # Construct a new alert structure.
        #
        alert = _get_alert( true )

        #
        #  We're raising this alert.
        #
        alert.raise_time = Time.now.to_i

        #
        # The supression period varies depending on the time of day.
        #
        hour = Time.now.hour
        wday = Time.now.wday

        #
        # Is this inside the working day?
        #
        working = false

        #
        # Lookup the start of the day.
        #
        day_start = @settings.key( "day_start" ).to_i || 10
        day_end   = @settings.key( "day_end" ).to_i   || 18

        #
        #  In hour suppress
        #
        working_suppress = @settings.key( "working_suppress" ).to_i || 4
        oncall_suppress  = @settings.key( "oncall_suppress" ).to_i  || 10

        #
        # If we're Monday-Friday, between the start & end time, then
        # we're in the working day.
        #
        if ( ( ( wday != 0 ) && ( wday != 6 ) ) &&
             ( hour >= day_start && hour < day_end ) )
          working = true
        end

        #
        # The suppression period can now be determined.
        #
        period = working ? working_suppress : oncall_suppress

        #
        # And logged.
        #
        puts "Suppression period is #{period}m"

        #
        # We're going to suppress this alert now
        #
        alert.suppress_until = Time.now.to_i + ( period * 60 )

        #
        #  Update it and send it
        #
        update.alert << alert
        Mauve::Sender.new( @target ).send(update)

      end



      #
      # Generate an alert-message which will be cleared via mauve.
      #
      def clear

        return unless( @loaded )

        #
        # Get ready to send to mauve.
        #
        update = Mauve::Proto::AlertUpdate.new
        update.alert   = []
        update.source  = @settings.alert_source()
        update.replace = false

        #
        # Construct a new alert structure.
        #
        alert = _get_alert( false )

        #
        #  We're clearing this alert.
        #
        alert.clear_time = Time.now.to_i

        #
        #  Update it and send it
        #
        update.alert << alert
        Mauve::Sender.new( @target ).send(update)

      end






      #
      # Using the test object, which was set in the constructor,
      # generate a useful alert that can be fired off to mauve.
      #
      # Most of the mess of this method is ensuring there is some
      # "helpful" data in the detail-field of the alert.
      #
      def _get_alert( failure )

        #
        # The subject of an alert MUST be one of:
        #
        #   1.  Hostname.
        #   2.  IP address
        #   3.  A URL.
        #
        # We attempt to resolve the alert to the hostname, as that is more
        # readable, if we have been given an IP address.
        #
        subject = @test.target

        if ( ( subject =~ /^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/ ) ||
             ( subject =~ /^([0-9a-f:]+)$/ ) )
          res = Custodian::Util::DNS.ip_to_hostname( subject )
          if ( res )
            subject = res
          end
        end


        #
        #  The test type + test target
        #
        test_host = test.target
        test_type = test.get_type

        alert         = Mauve::Proto::Alert.new

        #
        # Mauve only lets us use IDs which are <= 255 characters in length
        # hash the line from the parser to ensure it is short enough.
        # (IDs must be unique, per-source)
        #
        # Because there might be N-classes which implemented the test
        # we need to make sure these are distinct too.
        #
        id_key  = test.to_s
        id_key += test.class.to_s

        alert.id = Digest::SHA1.hexdigest(id_key)

        alert.subject = subject
        alert.summary = "The #{test_type} test failed against #{test_host}"

        #
        #  If we're raising then add the error
        #
        if ( failure )

          alert.detail  = "<p>The #{test_type} test failed against #{test_host}.</p>"

          #
          #  The text from the job-defition
          #
          user_text = test.get_notification_text()

          #
          # Add the user-detail if present
          #
          alert.detail = "#{alert.detail}<p>#{user_text}</p>" if ( !user_text.nil? )

          #
          # Add the test-failure message
          #
          alert.detail = "#{alert.detail}<p>#{test.error()}</p>"

          #
          #  Determine if this is inside/outside the bytemark network
          #
          location = expand_inside_bytemark( test_host )
          if ( !location.nil? && location.length )
            alert.detail = "#{alert.detail}\n#{location}"
          end
        end

        #
        # Return the alert to the caller.
        #
        alert
      end


      #
      # Expand to a message indicating whether a hostname is inside the Bytemark network.
      # or not.
      #
      #
      def expand_inside_bytemark( host )

        #
        #  If the host is a URL then we need to work with the hostname component alone.
        #
        #  We'll also make the host a link that can be clicked in the alert we raise.
        #
        target = host
        if ( target =~ /^([a-z]+):\/\/([^\/]+)/ )
          target = $2.dup
          host   = "<a href=\"#{host}\">#{host}</a>"
        end


        #
        #  Resolved IP of the target
        #
        resolved = nil

        #
        #  Resolve the target to an IP, unless it is already an address.
        #
        if ( ( target =~ /^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/ ) ||
             ( target =~ /^([0-9a-f:]+)$/ ) )
          resolved = target
        else
          resolved = Custodian::Util::DNS.hostname_to_ip( target )
        end


        #
        # Did we get an error?
        #
        return "" unless ( !resolved.nil? )


        #
        #  Return the formatted message
        #
        if ( Custodian::Util::Bytemark.inside?( resolved.to_s ) )
          if ( resolved == target )
            return "<p>#{host} is inside the Bytemark network.</p>"
          else
            return "<p>#{host} resolves to #{resolved} which is inside the Bytemark network.</p>"
          end
        else
          if ( resolved == target )
            return "<p>#{host} is OUTSIDE the Bytemark network.</p>"
          else
            return "<p>#{host} resolves to #{resolved} which is OUTSIDE the Bytemark network.</p>"
          end
        end

      end


      register_alert_type "mauve"




    end
  end
end