# frozen_string_literal: true

require "time"

module Pagerduty
  # Trigger incidents via the PagerDuty Events API version 2.
  #
  # @see https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTgw-events-api-v2-overview
  #   PagerDuty Events API V2 documentation
  #
  # @see Pagerduty.build
  #
  # @see Pagerduty::EventsApiV2::Incident
  #
  class EventsApiV2
    # Rather than using this directly, use the {Pagerduty.build} method to
    # construct an instance.
    #
    # @option config [String] integration_key Authentication key for connecting
    #   to PagerDuty. A UUID expressed as a 32-digit hexadecimal number.
    #   Integration keys are generated by creating a new service, or creating a
    #   new integration for an existing service in PagerDuty, and can be found
    #   on a service's Integrations tab. This option is required.
    #
    # @option config [String] http_proxy.host The DNS name or IP address of the
    #   proxy host. If nil or unprovided an HTTP proxy will not be used.
    #
    # @option config [String] http_proxy.port The TCP port to use to access the
    #   proxy.
    #
    # @option config [String] http_proxy.username username if authorization is
    #   required to use the proxy.
    #
    # @option config [String] http_proxy.password password if authorization is
    #   required to use the proxy.
    #
    # @see Pagerduty.build
    #
    def initialize(config = {})
      @config = config
    end

    # Send PagerDuty a trigger event to report a new or ongoing problem. When
    # PagerDuty receives a trigger event, it will either open a new incident, or
    # add a new trigger log entry to an existing incident, depending on the
    # incident key.
    #
    # @example Trigger an incident, providing only required details
    #   incident = pagerduty.trigger(
    #     summary:  "summary",
    #     source:   "source",
    #     severity: "critical"
    #   )
    #
    # @example Trigger an incident providing full context
    #    incident = pagerduty.trigger(
    #      summary:        "Example alert on host1.example.com",
    #      source:         "monitoringtool:host1.example.com/prod-003",
    #      severity:       %w[critical error warning info].sample,
    #      timestamp:      Time.now,
    #      component:      "postgres",
    #      group:          "prod-datapipe",
    #      class:          "deploy",
    #      custom_details: {
    #                        ping_time: "1500ms",
    #                        load_avg:  0.75
    #                      },
    #      images:         [
    #                        {
    #                          src:  "https://chart.googleapis.com/chart.png",
    #                          href: "https://example.com/",
    #                          alt:  "Example text",
    #                        },
    #                      ],
    #      links:          [
    #                        {
    #                          href: "https://example.com/",
    #                          text: "Link text",
    #                        },
    #                      ],
    #      client:         "Sample Monitoring Service",
    #      client_url:     "https://monitoring.example.com"
    #    )
    #
    # @option details [String] summary A brief text summary of the event,
    #   used to generate the summaries/titles of any associated alerts.
    #   The maximum permitted length of this property is 1024 characters.
    #
    # @option details [String] source The unique location of the affected
    #   system, preferably a hostname or FQDN.
    #
    # @option details [String] severity The perceived severity of the status
    #   the event is describing with respect to the affected system. This can
    #   be "critical", "error", "warning" or "info".
    #
    # @option details [Time] timestamp The time at which the emitting tool
    #   detected or generated the event.
    #
    # @option details [String] component Component of the source machine
    #   that is responsible for the event, for example "mysql" or "eth0".
    #
    # @option details [String] group Logical grouping of components of a
    #   service, for example "app-stack".
    #
    # @option details [String] class The class/type of the event, for
    #   example "ping failure" or "cpu load".
    #
    # @option details [Hash] custom_details Additional details about the
    #   event and affected system
    #
    # @option details [Array] images List of images to include.
    #
    # @option details [Array] links List of links to include.
    #
    # @return [Pagerduty::EventsApiV2::Incident] The triggered incident.
    #
    # @raise [PagerdutyException] If PagerDuty responds with a status that is
    #   not "success"
    #
    # @raise [ArgumentError] If details hash is nil
    #
    def trigger(details)
      Incident.new(@config).trigger(details)
    end

    # @param [String] incident_key The unique identifier for the incident.
    #
    # @return [Pagerduty::EventsApiV2::Incident] The incident referenced by the
    #   provided key.
    #
    # @raise [ArgumentError] If incident_key is nil
    #
    def incident(incident_key)
      raise ArgumentError, "incident_key is nil" if incident_key.nil?

      Incident.new(@config.merge(incident_key: incident_key))
    end

    class Incident
      attr_reader :incident_key

      # @option (see Pagerduty::EventsApiV1#initialize)
      #
      # @option config [String] incident_key Identifies the incident to which
      #   this trigger event should be applied. If there's no open
      #   (i.e. unresolved) incident with this key, a new one will be created.
      #   If there's already an open incident with a matching key, this event
      #   will be appended to that incident's log. The event key provides an
      #   easy way to "de-dup" problem reports. If this field isn't provided,
      #   PagerDuty will automatically open a new incident with a unique key.
      #   The maximum length is 255 characters.
      #
      def initialize(config = {})
        @integration_key = config.fetch(:integration_key) do
          raise ArgumentError "integration_key not provided"
        end
        @incident_key = config[:incident_key]
        @transport = Pagerduty::HttpTransport.new(
          path:  "/v2/enqueue",
          proxy: config[:http_proxy],
        )
      end

      # Send PagerDuty a trigger event to report a new or ongoing problem. When
      # PagerDuty receives a trigger event, it will either open a new incident,
      # or add a new trigger log entry to an existing incident, depending on
      # the incident key.
      #
      # @example Trigger an incident, providing only required details
      #   incident = pagerduty.trigger(
      #     summary:  "summary",
      #     source:   "source",
      #     severity: "critical"
      #   )
      #
      # @example Trigger an incident providing full context
      #    incident = pagerduty.trigger(
      #      summary:        "Example alert on host1.example.com",
      #      source:         "monitoringtool:host1.example.com/prod-003",
      #      severity:       %w[critical error warning info].sample,
      #      timestamp:      Time.now,
      #      component:      "postgres",
      #      group:          "prod-datapipe",
      #      class:          "deploy",
      #      custom_details: {
      #                        ping_time: "1500ms",
      #                        load_avg:  0.75
      #                      },
      #      images:         [
      #                        {
      #                          src:  "https://chart.googleapis.com/chart.png",
      #                          href: "https://example.com/",
      #                          alt:  "Example text",
      #                        },
      #                      ],
      #      links:          [
      #                        {
      #                          href: "https://example.com/",
      #                          text: "Link text",
      #                        },
      #                      ],
      #      client:         "Sample Monitoring Service",
      #      client_url:     "https://monitoring.example.com"
      #    )
      #
      # @param (see Pagerduty::EventsApiV2#trigger)
      # @option (see Pagerduty::EventsApiV2#trigger)
      def trigger(details)
        if details.key?(:dedup_key) || details.key?(:incident_key)
          raise ArgumentError, "incident_key or dedup_key provided, "\
                               "please use the EventsApiv2::incident method "\
                               "to specify an incident key"
        end

        response = api_call("trigger", trigger_request(details))
        @incident_key = response["dedup_key"]
        self
      end

      # Acknowledge the referenced incident. While an incident is acknowledged,
      # it won't generate any additional notifications, even if it receives new
      # trigger events. Send PagerDuty an acknowledge event when you know
      # someone is presently working on the problem.
      #
      # @return [Pagerduty::EventsApiV2::Incident] self
      #
      # @raise [PagerdutyException] If PagerDuty responds with a status that is
      #   not "success"
      #
      def acknowledge
        api_call("acknowledge")
        self
      end

      # Resolve the referenced incident. Once an incident is resolved, it won't
      # generate any additional notifications. New trigger events with the same
      # incident_key as a resolved incident won't re-open the incident. Instead,
      # a new incident will be created. Send PagerDuty a resolve event when the
      # problem that caused the initial trigger event has been fixed.
      #
      # @return [Pagerduty::EventsApiV2::Incident] self
      #
      # @raise [PagerdutyException] If PagerDuty responds with a status that is
      #   not "success"
      #
      def resolve
        api_call("resolve")
        self
      end

      private

      PAYLOAD_ATTR = %i[summary timestamp source severity
                        component group class custom_details].freeze
      private_constant :PAYLOAD_ATTR

      def trigger_request(details)
        payload = details.select { |key| PAYLOAD_ATTR.include?(key) }
        payload[:timestamp] &&= payload[:timestamp].iso8601
        request = details.merge(payload: payload)
        request.reject! { |key| PAYLOAD_ATTR.include?(key) }
        request
      end

      def api_call(event_action, payload = {})
        payload = payload.merge(
          dedup_key:    incident_key,
          routing_key:  @integration_key,
          event_action: event_action,
        )
        response = @transport.send_payload(payload)
        raise PagerdutyException.new(self, response, response["message"]) unless response["status"] == "success"

        response
      end
    end
  end
end
