require 'open3'
require 'flydata-core/table_def/mysql_table_def'

module FlydataCore
  module Mysql
    class CommandGenerator
      DEFAULT_MYSQL_CMD_OPTION = "--default-character-set=utf8 --protocol=tcp"

      # Generate mysql/mysqldump command with options
      # options must be hash
      # - command # mysql(default) | mysqldump
      # - host
      # - port
      # - username
      # - password
      # - database
      # - tables  # array
      # - ssl_ca
      # - custom_option   # string
      def self.generate_mysql_cmd(option)
        raise ArgumentError.new("option must be hash.") unless option.kind_of?(Hash)
        option = convert_keys_to_sym(option)
        command = option[:command] ? option[:command] : 'mysql'
        host = option[:host] ? "-h #{option[:host]}" : nil
        port = option[:port] ? "-P #{option[:port]}" : nil
        username = option[:username] ? "-u#{option[:username]}" : nil
        password = if !(option[:password].to_s.empty?)
                     "-p\"#{option[:password].gsub('$','\\$').gsub('"','\\"').gsub('`', '\\\`')}\""
                   else
                     nil
                   end
        database = option[:database]
        tables = option[:tables] ? option[:tables].join(' ') : nil
        ssl_ca = option[:ssl_ca] ? option[:ssl_ca] : nil

        default_option = option[:no_default_option] ? "" : DEFAULT_MYSQL_CMD_OPTION
        default_option += " --ssl-ca=#{ssl_ca}" if ssl_ca
        default_option = nil if default_option == ''

        custom_option = option[:custom_option]

        [command, host, port, username, password, default_option,
         custom_option, database, tables].compact.join(' ')
      end

      # DDL_DUMP_CMD_TEMPLATE = "MYSQL_PWD=\"%s\" mysqldump --protocol=tcp -d -h %s -P %s -u %s %s %s"
      def self.generate_mysql_ddl_dump_cmd(option)
        opt = option.dup
        opt[:command] = 'mysqldump'
        opt[:custom_option] = '-d'
        generate_mysql_cmd(opt)
      end

      # MYSQL_DUMP_CMD_TEMPLATE = "MYSQL_PWD=\"%s\" mysqldump --default-character-set=utf8 --protocol=tcp -h %s -P %s -u%s --skip-lock-tables --single-transaction --hex-blob %s %s %s"
      def self.generate_mysqldump_with_master_data_cmd(option)
        opt = option.dup
        opt[:command] = 'mysqldump'
        opt[:custom_option] = '--skip-lock-tables --single-transaction --hex-blob --flush-logs --master-data=2'
        generate_mysql_cmd(opt)
      end

      def self.generate_mysqldump_without_master_data_cmd(option)
        opt = option.dup
        opt[:command] = 'mysqldump'
        opt[:custom_option] = '--skip-lock-tables --single-transaction --hex-blob'
        if opt[:result_file]
          opt[:custom_option] << " --result-file=#{opt[:result_file]}"
        end
        generate_mysql_cmd(opt)
      end

      def self.generate_mysql_show_grants_cmd(option)
        opt = option.dup
        opt[:command] = 'mysql'
        opt[:custom_option] = '-e "SHOW GRANTS;"'
        generate_mysql_cmd(opt)
      end

      def self.each_mysql_tabledef(tables, options, &block)
        tables = tables.clone
        missing_tables = []
        begin
          if tables.to_s == '' || tables.to_s == '[]'
            raise ArgumentError, "tables is nil or empty"
          end
          _each_mysql_tabledef(tables, options, &block)
        rescue TableMissingError => e
          tables.delete e.table
          missing_tables << e.table
          return missing_tables if tables.empty?
          retry
        end
        missing_tables
      end

      private

      class TableMissingError < RuntimeError
        def initialize(message, table)
          super(message)
          @table = table
        end
         attr_reader :table
      end

      def self._each_mysql_tabledef(tables, option)
        command = generate_mysql_ddl_dump_cmd(option.merge(tables: tables))

        create_opt = {}
        if option.has_key?(:skip_primary_key_check)
          create_opt[:skip_primary_key_check] = option[:skip_primary_key_check]
        end

        Open3.popen3(command) do |stdin, stdout, stderr|
          stdin.close
          stdout.set_encoding("utf-8", "utf-8") # mysqldump output must be in UTF-8
          create_flydata_ctl_table = true
          while !stdout.eof?
            begin
              mysql_tabledef = FlydataCore::TableDef::MysqlTableDef.create(stdout, create_opt)
              break if mysql_tabledef.nil?
              yield(mysql_tabledef, nil)
            rescue FlydataCore::TableDefError=> e
              yield(nil, e)
            end
          end
          errors = ""
          while !stderr.eof?
            line = stderr.gets.gsub('mysqldump: ', '')
            case line
            when /Couldn't find table: "([^"]+)"/
              missing_table = $1
              raise TableMissingError.new(line, missing_table)
            when /Warning: Using a password on the command line interface can be insecure./
              # Ignore
            else
              errors << line
            end
          end
          raise errors unless errors.empty?
        end
      end

      def self.convert_keys_to_sym(hash)
        hash.inject(hash.dup) do |ret, (k, v)|
          if k.kind_of?(String) && ret[k.to_sym].nil?
            ret[k.to_sym] = v
          end
          ret
        end
      end
    end
  end
end