# Copyright (c) 2021 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/interface'

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::Interface
        access_component :config, :contrast_service, :logging

        def initialize
          @socket = init_connection
        end

        # Wrap the given DTM in a Contrast::Api::Dtm::Message and send it to the
        # Service for processing
        #
        # @param event [Contrast::Api::Dtm] One of the DTMs valid for the event field of
        #   Contrast::Api::Dtm::Message
        # @return [Contrast::Api::Settings::AgentSettings]
        def send_one event
          msg = Contrast::Api::Dtm::Message.build(event)
          send_message(msg)
        end

        private

        def init_connection
          log_connection
          if CONTRAST_SERVICE.use_tcp?
            Contrast::Api::Communication::TcpSocket.new(CONTRAST_SERVICE.host, CONTRAST_SERVICE.port)
          else
            Contrast::Api::Communication::UnixSocket.new(CONTRAST_SERVICE.socket_path)
          end
        end

        def log_connection
          # The socket is set,
          if CONFIG.root.agent.service.socket
            logger.info('Connecting to the Contrast Service using a UnixSocket socket',
                        socket: CONTRAST_SERVICE.socket_path)
            return
          end
          # The host & port are set,
          if CONFIG.root.agent.service.host && CONFIG.root.agent.service.port
            logger.info('Connecting to the Contrast Service using a TCP socket',
                        host: CONTRAST_SERVICE.host,
                        port: CONTRAST_SERVICE.port)
            return
          end

          # Or something is not set.
          logger.warn(log_connection_error_msg,
                      host: CONTRAST_SERVICE.host,
                      port: 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 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 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

        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('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

        def send_marshaled marshaled
          @socket.send_marshaled(marshaled)
        end
      end
    end
  end
end