module IqSMS
  class Client
    @mutex = Mutex.new

    def self.connection_pool_for(url)
      @mutex.synchronize do
        @connection_pool ||= {}
        if @connection_pool[url].blank?
          @connection_pool[url] = ConnectionPool.new(size: 5, timeout: 5) do
            HTTP.persistent(url)
          end
        end
        @connection_pool[url]
      end
    end

    attr_reader :connection
    attr_writer :status_queue_name

    def initialize(login:, password:, **options)
      with_value_must_be_present!(login) { @login = login }
      with_value_must_be_present!(password) { @password = password }

      @options = options.reverse_merge(default_options)

      check_status_queue_name!
    end

    def send_sms(messages)
      messages = Array(messages)
      check_messages_size!(messages)

      options = {
        statusQueueName: status_queue_name,
        messages: messages.map { |message| Message.message_to_hash(message) }
      }

      request(:post, 'send'.freeze, options) do |response|
        Response::SendSms.new(response)
      end
    end

    def status(messages)
      messages = Array(messages)
      check_messages_size!(messages)

      options = {
        statusQueueName: status_queue_name,
        messages: messages.map { |message| Message.message_to_hash_for_status(message) }
      }

      request(:post, 'status'.freeze, options) do |response|
        Response::Status.new(response)
      end
    end

    def status_queue(status_queue_limit = 5)
      if status_queue_name.blank?
        raise ArgumentError, 'status_queue_name must be set to use status_queue endpoint'
      end

      options = {
        statusQueueName: status_queue_name,
        statusQueueLimit: status_queue_limit
      }

      request(:post, 'statusQueue'.freeze, options) do |response|
        Response::StatusQueue.new(response)
      end
    end

    def balance
      request(:post, 'messages/v2/balance'.freeze) do |response|
        Response::Balance.new(response)
      end
    end

    def senders
      request(:post, 'senders'.freeze) do |response|
        Response::Senders.new(response)
      end
    end

    def ping?
      request(:post, 'messages/v2/balance'.freeze) do |response|
        response.status.success?
      end
    end

    def status_queue_name
      @status_queue_name ||= @options[:status_queue_name]
    end

    def status_queue_name=(value)
      @status_queue_name = value
    end

    private

    def request(method, path, **params)
      params = params.nil? ? {} : params.dup

      params = params.reverse_merge!(authentication_params)

      self.class.connection_pool_for(base_url).with do |connection|
        begin
          retries ||= 0

          response = connection.headers(accept: 'application/json'.freeze)
                               .send(method, full_url(path), json: params)

          block_given? ? yield(response) : response
        rescue HTTP::StateError => error
          retries += 1
          retries < 3 ? retry : raise(error)
        ensure
          response.flush if response.present?
          connection.close if connection.present?
        end
      end
    end

    def with_value_must_be_present!(value)
      raise ArgumentError, "can't be blank".freeze if value.blank?
      yield
    end

    def check_status_queue_name!
      queue_name = @options[:status_queue_name]
      return unless queue_name.present?

      unless queue_name.is_a?(String)
        raise ArgumentError, 'status_queue_name must be a string'.freeze
      end
      if queue_name.size < 3 || queue_name.size > 16
        raise ArgumentError, 'status_queue_name length must be in between 3 and 16 chars'.freeze
      end
      unless queue_name =~ /[a-zA-Z0-9]+/
        raise ArgumentError, 'status_queue_name must be alphanumeric only'.freeze
      end
    end

    def check_messages_size!(messages)
      return if messages.size <= 200

      raise MaximumMessagesLimitExceededError, 'API has cap of 200 messages per one request'
    end

    def default_options
      {
        base_url: 'http://json.gate.iqsms.ru'.freeze
      }
    end

    def authentication_params
      {
        login: @login,
        password: @password
      }
    end

    def base_url
      @options[:base_url] || self.class.default_base_url
    end

    def base_uri
      @base_uri ||= Addressable::URI.parse(base_url)
      @base_uri
    end

    def full_url(path)
      uri = base_uri
      uri.path = path
      uri.to_s
    end
  end
end