# frozen_string_literal: true

module RCAP
  module Base
    class Alert
      include Validation

      STATUS_ACTUAL   = 'Actual'
      STATUS_EXERCISE = 'Exercise'
      STATUS_SYSTEM   = 'System'
      STATUS_TEST     = 'Test'
      # Valid values for status
      VALID_STATUSES = [STATUS_ACTUAL, STATUS_EXERCISE, STATUS_SYSTEM, STATUS_TEST].freeze

      MSG_TYPE_ALERT  = 'Alert'
      MSG_TYPE_UPDATE = 'Update'
      MSG_TYPE_CANCEL = 'Cancel'
      MSG_TYPE_ACK    = 'Ack'
      MSG_TYPE_ERROR  = 'Error'
      # Valid values for msg_type
      VALID_MSG_TYPES = [MSG_TYPE_ALERT, MSG_TYPE_UPDATE, MSG_TYPE_CANCEL, MSG_TYPE_ACK, MSG_TYPE_ERROR].freeze

      SCOPE_PUBLIC     = 'Public'
      SCOPE_RESTRICTED = 'Restricted'
      SCOPE_PRIVATE    = 'Private'
      # Valid values for scope
      VALID_SCOPES = [SCOPE_PUBLIC, SCOPE_PRIVATE, SCOPE_RESTRICTED].freeze

      # @return [String] If not set a UUID will be set by default on object initialisation
      attr_accessor(:identifier)
      # @return [String]
      attr_accessor(:sender)
      # @return [DateTime] If not set will be time of creation.
      attr_accessor(:sent)
      # @return [String] Can only be one of {VALID_STATUSES}
      attr_accessor(:status)
      # @return [String] Can only be one of {VALID_MSG_TYPES}
      attr_accessor(:msg_type)
      # @return [String] Can only be one of {VALID_SCOPES}
      attr_accessor(:scope)
      # @return [String]
      attr_accessor(:source)
      # @return [String ] Required if scope is {SCOPE_RESTRICTED}
      attr_accessor(:restriction)
      # @return [String]
      attr_accessor(:note)

      # @return [Array<String>] Collection of address strings. Depends on scope being {SCOPE_PRIVATE}
      attr_reader(:addresses)
      # @return [Array<String>]
      attr_reader(:codes)
      # @return [Array<String>] Collection of references to previous alerts
      # @see #to_reference
      attr_reader(:references)
      # @return [Array<String>] Collection of incident strings
      attr_reader(:incidents)
      # @return [Array<Info>]
      attr_reader(:infos)

      validates_presence_of(:identifier, :sender, :sent, :status, :msg_type, :scope)

      validates_inclusion_of(:status,   in: VALID_STATUSES)
      validates_inclusion_of(:msg_type, in: VALID_MSG_TYPES)
      validates_inclusion_of(:scope,    in: VALID_SCOPES)

      validates_format_of(:identifier, with: ALLOWED_CHARACTERS)
      validates_format_of(:sender,     with: ALLOWED_CHARACTERS)

      validates_conditional_presence_of(:addresses,   when: :scope, is: SCOPE_PRIVATE)
      validates_conditional_presence_of(:restriction, when: :scope, is: SCOPE_RESTRICTED)

      validates_collection_of(:infos)

      # Initialises a new Alert object. Yields the initialised alert to a block.
      #
      # @example
      #   alert = RCAP::CAP_1_2::Alert.new do |alert|
      #             alert.sender = alerts@example.org
      #             alert.status = Alert::STATUS_ACTUAL
      #             alert.msg_type = Alert::MSG_TYPE_ALERT
      #             alert.scope = Alert::SCOPE_PUBLIC
      #           end
      #
      # @yieldparam alert [Alert] The newly initialised Alert.
      def initialize
        @identifier  = RCAP.generate_identifier
        @addresses   = []
        @codes       = []
        @references  = []
        @incidents   = []
        @infos       = []
        yield(self) if block_given?
      end

      # Creates a new {Info} object and adds it to the {#infos} array.
      #
      # @see Info#initialize
      # @yield [Info] The newly initialised Info object.
      # @return [Info] The initialised Info object after being yielded to the block
      def add_info
        info_class.new.tap do |info|
          yield(info) if block_given?
          @infos << info
        end
      end

      XML_ELEMENT_NAME         = 'alert'
      IDENTIFIER_ELEMENT_NAME  = 'identifier'
      SENDER_ELEMENT_NAME      = 'sender'
      SENT_ELEMENT_NAME        = 'sent'
      STATUS_ELEMENT_NAME      = 'status'
      MSG_TYPE_ELEMENT_NAME    = 'msgType'
      SOURCE_ELEMENT_NAME      = 'source'
      SCOPE_ELEMENT_NAME       = 'scope'
      RESTRICTION_ELEMENT_NAME = 'restriction'
      ADDRESSES_ELEMENT_NAME   = 'addresses'
      CODE_ELEMENT_NAME        = 'code'
      NOTE_ELEMENT_NAME        = 'note'
      REFERENCES_ELEMENT_NAME  = 'references'
      INCIDENTS_ELEMENT_NAME   = 'incidents'

      # @return [REXML::Element]
      def to_xml_element
        xml_element = REXML::Element.new(XML_ELEMENT_NAME)
        xml_element.add_namespace(self.class::XMLNS)
        xml_element.add_element(IDENTIFIER_ELEMENT_NAME).add_text(@identifier.to_s)   if @identifier
        xml_element.add_element(SENDER_ELEMENT_NAME).add_text(@sender.to_s)           if @sender
        xml_element.add_element(SENT_ELEMENT_NAME).add_text(@sent.to_s_for_cap)       if @sent
        xml_element.add_element(STATUS_ELEMENT_NAME).add_text(@status.to_s)           if @status
        xml_element.add_element(MSG_TYPE_ELEMENT_NAME).add_text(@msg_type.to_s)       if @msg_type
        xml_element.add_element(SOURCE_ELEMENT_NAME).add_text(@source.to_s)           if @source
        xml_element.add_element(SCOPE_ELEMENT_NAME).add_text(@scope.to_s)             if @scope
        xml_element.add_element(RESTRICTION_ELEMENT_NAME).add_text(@restriction.to_s) if @restriction
        xml_element.add_element(ADDRESSES_ELEMENT_NAME).add_text(@addresses.to_s_for_cap) if @addresses.any?
        @codes.each do |code|
          xml_element.add_element(CODE_ELEMENT_NAME).add_text(code.to_s)
        end
        xml_element.add_element(NOTE_ELEMENT_NAME).add_text(@note.to_s) if @note
        xml_element.add_element(REFERENCES_ELEMENT_NAME).add_text(@references.join(' ')) if @references.any?
        xml_element.add_element(INCIDENTS_ELEMENT_NAME).add_text(@incidents.join(' ')) if @incidents.any?
        @infos.each do |info|
          xml_element.add_element(info.to_xml_element)
        end
        xml_element
      end

      # @return [REXML::Document]
      def to_xml_document
        xml_document = REXML::Document.new
        xml_document.add(REXML::XMLDecl.new)
        xml_document.add(to_xml_element)
        xml_document
      end

      # Returns a string containing the XML representation of the alert.
      #
      # @param [true,false] pretty_print Pretty print output
      # @return [String]
      def to_xml(pretty_print = false)
        if pretty_print
          xml_document = +''
          RCAP::XML_PRETTY_PRINTER.write(to_xml_document, xml_document)
          xml_document
        else
          to_xml_document.to_s
        end
      end

      # Returns a string representation of the alert suitable for usage as a reference in a CAP message of the form
      #  sender,identifier,sent
      #
      # @return [String]
      def to_reference
        "#{@sender},#{@identifier},#{RCAP.to_s_for_cap(@sent)}"
      end

      # @return [String]
      def inspect
        alert_inspect = ["CAP Version:  #{self.class::CAP_VERSION}",
                         "Identifier:   #{@identifier}",
                         "Sender:       #{@sender}",
                         "Sent:         #{@sent}",
                         "Status:       #{@status}",
                         "Message Type: #{@msg_type}",
                         "Source:       #{@source}",
                         "Scope:        #{@scope}",
                         "Restriction:  #{@restriction}",
                         "Addresses:    #{@addresses.to_s_for_cap}",
                         'Codes:',
                         @codes.map { |code| '  ' + code }.join("\n") + '',
                         "Note:         #{@note}",
                         'References:',
                         @references.join("\n "),
                         "Incidents:    #{@incidents.join(' ')}",
                         'Information:',
                         @infos.map { |info| '  ' + info.to_s }.join("\n")].join("\n")
        RCAP.format_lines_for_inspect('ALERT', alert_inspect)
      end

      # Returns a string representation of the alert of the form
      #  sender/identifier/sent
      # See {#to_reference} for another string representation suitable as a CAP reference.
      #
      # @return [String]
      def to_s
        "#{@sender}/#{@identifier}/#{RCAP.to_s_for_cap(@sent)}"
      end

      XPATH             = 'cap:alert'
      IDENTIFIER_XPATH  = "cap:#{IDENTIFIER_ELEMENT_NAME}"
      SENDER_XPATH      = "cap:#{SENDER_ELEMENT_NAME}"
      SENT_XPATH        = "cap:#{SENT_ELEMENT_NAME}"
      STATUS_XPATH      = "cap:#{STATUS_ELEMENT_NAME}"
      MSG_TYPE_XPATH    = "cap:#{MSG_TYPE_ELEMENT_NAME}"
      SOURCE_XPATH      = "cap:#{SOURCE_ELEMENT_NAME}"
      SCOPE_XPATH       = "cap:#{SCOPE_ELEMENT_NAME}"
      RESTRICTION_XPATH = "cap:#{RESTRICTION_ELEMENT_NAME}"
      ADDRESSES_XPATH   = "cap:#{ADDRESSES_ELEMENT_NAME}"
      CODE_XPATH        = "cap:#{CODE_ELEMENT_NAME}"
      NOTE_XPATH        = "cap:#{NOTE_ELEMENT_NAME}"
      REFERENCES_XPATH  = "cap:#{REFERENCES_ELEMENT_NAME}"
      INCIDENTS_XPATH   = "cap:#{INCIDENTS_ELEMENT_NAME}"

      # @param [REXML::Element] alert_xml_element
      # @return [RCAP::CAP_1_0::Alert]
      def self.from_xml_element(alert_xml_element)
        new do |alert|
          alert.identifier  = RCAP.xpath_text(alert_xml_element, IDENTIFIER_XPATH, alert.xmlns)
          alert.sender      = RCAP.xpath_text(alert_xml_element, SENDER_XPATH, alert.xmlns)
          alert.sent        = RCAP.parse_datetime(RCAP.xpath_text(alert_xml_element, SENT_XPATH, alert.xmlns))
          alert.status      = RCAP.xpath_text(alert_xml_element, STATUS_XPATH, alert.xmlns)
          alert.msg_type    = RCAP.xpath_text(alert_xml_element, MSG_TYPE_XPATH, alert.xmlns)
          alert.source      = RCAP.xpath_text(alert_xml_element, SOURCE_XPATH, alert.xmlns)
          alert.scope       = RCAP.xpath_text(alert_xml_element, SCOPE_XPATH, alert.xmlns)
          alert.restriction = RCAP.xpath_text(alert_xml_element, RESTRICTION_XPATH, alert.xmlns)

          RCAP.unpack_if_given(RCAP.xpath_text(alert_xml_element, ADDRESSES_XPATH, alert.xmlns)).each do |address|
            alert.addresses << address.strip
          end

          RCAP.xpath_match(alert_xml_element, CODE_XPATH, alert.xmlns).each do |element|
            alert.codes << element.text.strip
          end

          alert.note = RCAP.xpath_text(alert_xml_element, NOTE_XPATH, alert.xmlns)

          RCAP.unpack_if_given(RCAP.xpath_text(alert_xml_element, REFERENCES_XPATH, alert.xmlns)).each do |reference|
            alert.references << reference.strip
          end

          RCAP.unpack_if_given(RCAP.xpath_text(alert_xml_element, INCIDENTS_XPATH, alert.xmlns)).each do |incident|
            alert.incidents << incident.strip
          end

          RCAP.xpath_match(alert_xml_element, Info::XPATH, alert.xmlns).each do |element|
            alert.infos << alert.info_class.from_xml_element(element)
          end
        end
      end

      # @param [REXML::Document] xml_document
      # @return [Alert]
      def self.from_xml_document(xml_document)
        from_xml_element(xml_document.root)
      end

      # Initialise an Alert object from an XML string. Any object that is a subclass of IO (e.g. File) can be passed in.
      #
      # @param [String] xml
      # @return [Alert]
      def self.from_xml(xml)
        from_xml_document(REXML::Document.new(xml))
      end

      CAP_VERSION_YAML = 'CAP Version'
      IDENTIFIER_YAML  = 'Identifier'
      SENDER_YAML      = 'Sender'
      SENT_YAML        = 'Sent'
      STATUS_YAML      = 'Status'
      MSG_TYPE_YAML    = 'Message Type'
      SOURCE_YAML      = 'Source'
      SCOPE_YAML       = 'Scope'
      RESTRICTION_YAML = 'Restriction'
      ADDRESSES_YAML   = 'Addresses'
      CODES_YAML       = 'Codes'
      NOTE_YAML        = 'Note'
      REFERENCES_YAML  = 'References'
      INCIDENTS_YAML   = 'Incidents'
      INFOS_YAML       = 'Information'

      # Returns a string containing the YAML representation of the alert.
      #
      # @return [String]
      def to_yaml(options = {})
        RCAP.attribute_values_to_hash([CAP_VERSION_YAML, self.class::CAP_VERSION],
                                      [IDENTIFIER_YAML,  @identifier],
                                      [SENDER_YAML,      @sender],
                                      [SENT_YAML,        @sent],
                                      [STATUS_YAML,      @status],
                                      [MSG_TYPE_YAML,    @msg_type],
                                      [SOURCE_YAML,      @source],
                                      [SCOPE_YAML,       @scope],
                                      [RESTRICTION_YAML, @restriction],
                                      [ADDRESSES_YAML,   @addresses],
                                      [CODES_YAML,       @codes],
                                      [NOTE_YAML,        @note],
                                      [REFERENCES_YAML,  @references],
                                      [INCIDENTS_YAML,   @incidents],
                                      [INFOS_YAML,       @infos.map(&:to_yaml_data)]).to_yaml(options)
      end

      # Initialise an Alert object from a YAML string. Any object that is a subclass of IO (e.g. File) can be passed in.
      #
      # @param [String] yaml
      # @return [Alert]
      def self.from_yaml(yaml)
        from_yaml_data(YAML.safe_load(yaml, [Time, DateTime]))
      end

      # Initialise an Alert object from a hash reutrned from YAML.load.
      #
      # @param [hash] alert_yaml_data
      # @return [Alert]
      def self.from_yaml_data(alert_yaml_data)
        new do |alert|
          alert.identifier  = RCAP.strip_if_given(alert_yaml_data[IDENTIFIER_YAML])
          alert.sender      = RCAP.strip_if_given(alert_yaml_data[SENDER_YAML])
          alert.sent        = RCAP.parse_datetime(alert_yaml_data[SENT_YAML])
          alert.status      = RCAP.strip_if_given(alert_yaml_data[STATUS_YAML])
          alert.msg_type    = RCAP.strip_if_given(alert_yaml_data[MSG_TYPE_YAML])
          alert.source      = RCAP.strip_if_given(alert_yaml_data[SOURCE_YAML])
          alert.scope       = RCAP.strip_if_given(alert_yaml_data[SCOPE_YAML])
          alert.restriction = RCAP.strip_if_given(alert_yaml_data[RESTRICTION_YAML])
          Array(alert_yaml_data[ADDRESSES_YAML]).each do |address|
            alert.addresses << address.strip
          end
          Array(alert_yaml_data[CODES_YAML]).each do |code|
            alert.codes << code.strip
          end
          alert.note = alert_yaml_data[NOTE_YAML]
          Array(alert_yaml_data[REFERENCES_YAML]).each do |reference|
            alert.references << reference.strip
          end
          Array(alert_yaml_data[INCIDENTS_YAML]).each do |incident|
            alert.incidents << incident.strip
          end
          Array(alert_yaml_data[INFOS_YAML]).each do |info_yaml_data|
            alert.infos <<  alert.info_class.from_yaml_data(info_yaml_data)
          end
        end
      end

      CAP_VERSION_KEY = 'cap_version'
      IDENTIFIER_KEY  = 'identifier'
      SENDER_KEY      = 'sender'
      SENT_KEY        = 'sent'
      STATUS_KEY      = 'status'
      MSG_TYPE_KEY    = 'msg_type'
      SOURCE_KEY      = 'source'
      SCOPE_KEY       = 'scope'
      RESTRICTION_KEY = 'restriction'
      ADDRESSES_KEY   = 'addresses'
      CODES_KEY       = 'codes'
      NOTE_KEY        = 'note'
      REFERENCES_KEY  = 'references'
      INCIDENTS_KEY   = 'incidents'
      INFOS_KEY       = 'infos'

      # Returns a Hash representation of an Alert object
      #
      # @return [Hash]
      def to_h
        RCAP.attribute_values_to_hash([CAP_VERSION_KEY, self.class::CAP_VERSION],
                                      [IDENTIFIER_KEY,  @identifier],
                                      [SENDER_KEY,      @sender],
                                      [SENT_KEY,        RCAP.to_s_for_cap(@sent)],
                                      [STATUS_KEY,      @status],
                                      [MSG_TYPE_KEY,    @msg_type],
                                      [SOURCE_KEY,      @source],
                                      [SCOPE_KEY,       @scope],
                                      [RESTRICTION_KEY, @restriction],
                                      [ADDRESSES_KEY,   @addresses],
                                      [CODES_KEY,       @codes],
                                      [NOTE_KEY,        @note],
                                      [REFERENCES_KEY,  @references],
                                      [INCIDENTS_KEY,   @incidents],
                                      [INFOS_KEY,       @infos.map(&:to_h)])
      end

      # Initialises an Alert object from a Hash produced by Alert#to_h
      #
      # @param [Hash] alert_hash
      # @return [RCAP::CAP_1_0::Alert]
      def self.from_h(alert_hash)
        new do |alert|
          alert.identifier  = RCAP.strip_if_given(alert_hash[IDENTIFIER_KEY])
          alert.sender      = RCAP.strip_if_given(alert_hash[SENDER_KEY])
          alert.sent        = RCAP.parse_datetime(alert_hash[SENT_KEY])
          alert.status      = RCAP.strip_if_given(alert_hash[STATUS_KEY])
          alert.msg_type    = RCAP.strip_if_given(alert_hash[MSG_TYPE_KEY])
          alert.source      = RCAP.strip_if_given(alert_hash[SOURCE_KEY])
          alert.scope       = RCAP.strip_if_given(alert_hash[SCOPE_KEY])
          alert.restriction = RCAP.strip_if_given(alert_hash[RESTRICTION_KEY])
          Array(alert_hash[ADDRESSES_KEY]).each do |address|
            alert.addresses << address.strip
          end
          Array(alert_hash[CODES_KEY]).each do |code|
            alert.codes << code.strip
          end
          alert.note = alert_hash[NOTE_KEY]
          Array(alert_hash[REFERENCES_KEY]).each do |reference|
            alert.references << reference.strip
          end

          Array(alert_hash[INCIDENTS_KEY]).each do |incident|
            alert.incidents << incident.strip
          end

          Array(alert_hash[INFOS_KEY]).each do |info_hash|
            alert.infos << alert.info_class.from_h(info_hash)
          end
        end
      end

      # Returns a JSON string representation of an Alert object
      #
      # @param [true,false] pretty_print
      # @return [String]
      def to_json(pretty_print = false)
        if pretty_print
          JSON.pretty_generate(to_h)
        else
          to_h.to_json
        end
      end

      # Initialises an Alert object from a JSON string produced by Alert#to_json
      #
      # @param [String] json_string
      # @return [Alert]
      def self.from_json(json_string)
        from_h(JSON.parse(json_string))
      end
    end
  end
end