module Mysql2
  class Client
    attr_reader :query_options, :read_timeout

    def self.default_query_options
      @default_query_options ||= {
        as: :hash,                   # the type of object you want each row back as; also supports :array (an array of values)
        async: false,                # don't wait for a result after sending the query, you'll have to monitor the socket yourself then eventually call Mysql2::Client#async_result
        cast_booleans: false,        # cast tinyint(1) fields as true/false in ruby
        symbolize_keys: false,       # return field names as symbols instead of strings
        database_timezone: :local,   # timezone Mysql2 will assume datetime objects are stored in
        application_timezone: nil,   # timezone Mysql2 will convert to before handing the object back to the caller
        cache_rows: true,            # tells Mysql2 to use its internal row cache for results
        connect_flags: REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION | CONNECT_ATTRS,
        cast: true,
        default_file: nil,
        default_group: nil,
      }
    end

    def initialize(opts = {})
      raise Mysql2::Error, "Options parameter must be a Hash" unless opts.is_a? Hash

      opts = Mysql2::Util.key_hash_as_symbols(opts)
      @read_timeout = nil
      @query_options = self.class.default_query_options.dup
      @query_options.merge! opts

      initialize_ext

      # Set default connect_timeout to avoid unlimited retries from signal interruption
      opts[:connect_timeout] = 120 unless opts.key?(:connect_timeout)

      # TODO: stricter validation rather than silent massaging
      %i[reconnect connect_timeout local_infile read_timeout write_timeout default_file default_group secure_auth init_command automatic_close enable_cleartext_plugin default_auth].each do |key|
        next unless opts.key?(key)

        case key
        when :reconnect, :local_infile, :secure_auth, :automatic_close, :enable_cleartext_plugin
          send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation
        when :connect_timeout, :read_timeout, :write_timeout
          send(:"#{key}=", Integer(opts[key])) unless opts[key].nil?
        else
          send(:"#{key}=", opts[key])
        end
      end

      # force the encoding to utf8
      self.charset_name = opts[:encoding] || 'utf8'

      mode = parse_ssl_mode(opts[:ssl_mode]) if opts[:ssl_mode]
      if (mode == SSL_MODE_VERIFY_CA || mode == SSL_MODE_VERIFY_IDENTITY) && !opts[:sslca]
        opts[:sslca] = find_default_ca_path
      end

      ssl_options = opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslcipher)
      ssl_set(*ssl_options) if ssl_options.any? || opts.key?(:sslverify)
      self.ssl_mode = mode if mode

      flags = case opts[:flags]
      when Array
        parse_flags_array(opts[:flags], @query_options[:connect_flags])
      when String
        parse_flags_array(opts[:flags].split(' '), @query_options[:connect_flags])
      when Integer
        @query_options[:connect_flags] | opts[:flags]
      else
        @query_options[:connect_flags]
      end

      # SSL verify is a connection flag rather than a mysql_ssl_set option
      flags |= SSL_VERIFY_SERVER_CERT if opts[:sslverify]

      if %i[user pass hostname dbname db sock].any? { |k| @query_options.key?(k) }
        warn "============= WARNING FROM mysql2 ============="
        warn "The options :user, :pass, :hostname, :dbname, :db, and :sock are deprecated and will be removed at some point in the future."
        warn "Instead, please use :username, :password, :host, :port, :database, :socket, :flags for the options."
        warn "============= END WARNING FROM mysql2 ========="
      end

      user     = opts[:username] || opts[:user]
      pass     = opts[:password] || opts[:pass]
      host     = opts[:host] || opts[:hostname]
      port     = opts[:port]
      database = opts[:database] || opts[:dbname] || opts[:db]
      socket   = opts[:socket] || opts[:sock]

      # Correct the data types before passing these values down to the C level
      user = user.to_s unless user.nil?
      pass = pass.to_s unless pass.nil?
      host = host.to_s unless host.nil?
      port = port.to_i unless port.nil?
      database = database.to_s unless database.nil?
      socket = socket.to_s unless socket.nil?
      conn_attrs = parse_connect_attrs(opts[:connect_attrs])

      connect user, pass, host, port, database, socket, flags, conn_attrs
    end

    def parse_ssl_mode(mode)
      m = mode.to_s.upcase
      if m.start_with?('SSL_MODE_')
        return Mysql2::Client.const_get(m) if Mysql2::Client.const_defined?(m)
      else
        x = 'SSL_MODE_' + m
        return Mysql2::Client.const_get(x) if Mysql2::Client.const_defined?(x)
      end
      warn "Unknown MySQL ssl_mode flag: #{mode}"
    end

    def parse_flags_array(flags, initial = 0)
      flags.reduce(initial) do |memo, f|
        fneg = f.start_with?('-') ? f[1..-1] : nil
        if fneg && fneg =~ /^\w+$/ && Mysql2::Client.const_defined?(fneg)
          memo & ~ Mysql2::Client.const_get(fneg)
        elsif f && f =~ /^\w+$/ && Mysql2::Client.const_defined?(f)
          memo | Mysql2::Client.const_get(f)
        else
          warn "Unknown MySQL connection flag: '#{f}'"
          memo
        end
      end
    end

    # Find any default system CA paths to handle system roots
    # by default if stricter validation is requested and no
    # path is provide.
    def find_default_ca_path
      [
        "/etc/ssl/certs/ca-certificates.crt",
        "/etc/pki/tls/certs/ca-bundle.crt",
        "/etc/ssl/ca-bundle.pem",
        "/etc/ssl/cert.pem",
      ].find { |f| File.exist?(f) }
    end

    # Set default program_name in performance_schema.session_connect_attrs
    # and performance_schema.session_account_connect_attrs
    def parse_connect_attrs(conn_attrs)
      return {} if Mysql2::Client::CONNECT_ATTRS.zero?

      conn_attrs ||= {}
      conn_attrs[:program_name] ||= $PROGRAM_NAME
      conn_attrs.each_with_object({}) do |(key, value), hash|
        hash[key.to_s] = value.to_s
      end
    end

    def query(sql, options = {})
      Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_NEVER) do
        _query(sql, @query_options.merge(options))
      end
    end

    def query_info
      info = query_info_string
      return {} unless info

      info_hash = {}
      info.split.each_slice(2) { |s| info_hash[s[0].downcase.delete(':').to_sym] = s[1].to_i }
      info_hash
    end

    def info
      self.class.info
    end

    class << self
      private

      def local_offset
        ::Time.local(2010).utc_offset.to_r / 86400
      end
    end
  end
end
