# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

require 'socket'
require 'uri'

require 'contrast/api/communication/tcp_socket'
require 'contrast/api/communication/unix_socket'
require 'contrast/components/logger'

module Contrast
  module Api
    module Communication
      # SocketClient acts as a interface between the agent and the service. It instantiates a service proxy and tracks
      # the state of that proxy.
      class SocketClient
        include Contrast::Components::Logger::InstanceMethods

        def initialize
          @socket = init_connection
        end

        # Wrap the given DTM in a Contrast::Api::Dtm::Message and send it to the SpeedRacer for processing. The
        # Message is the top level object required to communicate to SpeedRacer as it encompasses the information
        # needed to find this process' context.
        #
        # @param event [Contrast::Api::Dtm] One of the DTMs valid for the event field of
        #   Contrast::Api::Dtm::Message
        # @return [Contrast::Api::Settings::AgentSettings, nil]
        def send_one event
          msg = Contrast::Api::Dtm::Message.build(event)
          send_message(msg)
        end

        private

        # Initialize the connection to the SpeedRacer process based on the configuration provided by the user. This can
        # be either TCP or UDP. Note that unlike the Go and the Node Agents, we cannot use the GRPC communication
        # option as we cannot use Google's protobuf gems; they do not compile reliably and result in segmentation
        # faults in customer environments.
        #
        # @return [Contrast::Api::Communication::TcpSocket, Contrast::Api::Communication::UnixSocket]
        def init_connection
          log_connection
          if ::Contrast::CONTRAST_SERVICE.use_tcp?
            Contrast::Api::Communication::TcpSocket.new(
                ::Contrast::CONTRAST_SERVICE.host, ::Contrast::CONTRAST_SERVICE.port)
          else
            Contrast::Api::Communication::UnixSocket.new(::Contrast::CONTRAST_SERVICE.socket_path)
          end
        end

        # Log information about the connection being used to communicate between the Agent and SpeedRacer.
        def log_connection
          # The socket is set,
          if ::Contrast::CONFIG.root.agent.service.socket
            logger.info('Connecting to the Contrast Service using a UnixSocket socket',
                        socket: ::Contrast::CONTRAST_SERVICE.socket_path)
            return
          end
          # The host & port are set,
          if ::Contrast::CONFIG.root.agent.service.host && ::Contrast::CONFIG.root.agent.service.port
            logger.info('Connecting to the Contrast Service using a TCP socket',
                        host: ::Contrast::CONTRAST_SERVICE.host,
                        port: ::Contrast::CONTRAST_SERVICE.port)
            return
          end

          # Or something is not set.
          logger.warn(
              log_connection_error_msg,
              host: ::Contrast::CONTRAST_SERVICE.host,
              port: ::Contrast::CONTRAST_SERVICE.port)
        end

        # If our connection isn't built properly, we need to warn the user. This builds out the context specific
        # message to provide that warning
        #
        # @return [String]
        def log_connection_error_msg
          if ::Contrast::CONFIG.root.agent.service.host
            'Missing a required connection value to the Contrast Service. ' \
              '`agent.service.port` is not set. ' \
              'Falling back to default TCP socket port.'
          elsif ::Contrast::CONFIG.root.agent.service.port
            'Missing a required connection value to the Contrast Service. ' \
              '`agent.service.host` is not set. ' \
              'Falling back to default TCP socket host.'
          else
            'Missing a required connection value to the Contrast Service. ' \
              'Neither `agent.service.socket` nor the pair of `agent.service.host` and `agent.service.port` are set. '\
              'Falling back to default TCP socket.'
          end
        end

        # Send the given message to SpeedRacer and return the response from it.
        #
        # @param msg [Contrast::Api::Dtm::Message] the packaged message to send to SpeedRacer
        # @return [Contrast::Api::Settings::AgentSettings, nil]
        # @raise [StandardError] if unable to send a message to SpeedRacer
        def send_message msg
          return unless msg

          logger.debug('Sending message.', msg_id: msg.__id__, p_id: msg.pid, msg_count: msg.message_count)
          to_service = Contrast::Api::Dtm::Message.encode(msg)
          from_service = send_marshaled(to_service)
          response = Contrast::Api::Settings::AgentSettings.decode(from_service)
          logger.debug('RESPONSE FROM TS')
          logger.debug(from_service)
          logger.debug('RESPONSE FROM TS')
          logger.debug('Received response.', msg_id: msg.__id__, p_id: msg.pid, msg_count: msg.message_count,
                                             response_id: response&.__id__)
          response
        rescue StandardError => e
          logger.error('Sending failed for message.', e, msg_id: msg.__id__, p_id: msg.pid,
                                                         msg_count: msg.message_count, response_id: response&.__id__)
          raise(e) # reraise to let SpeedRacer manage the connection
        end

        # Send the marshaled Contrast::Api::Dtm::Message across the socket used to talk to SpeedRacer
        #
        # @param marshaled [String]
        # @return [String] the marshaled from of Contrast::Api::Settings::AgentSettings
        def send_marshaled marshaled
          @socket.send_marshaled(marshaled)
        end
      end
    end
  end
end