require 'tidy_reset/configuration'
require 'tidy_reset/version'
require 'open3'

module TidyReset
  class RakeExecuteException < StandardError; end
  class RakeStatusException < StandardError; end
  class RakeCompletedException < StandardError; end
 
  class << self
    attr_accessor :configuration

    def configuration
      @configuration ||= Configuration.new
    end

    def configure
      yield(configuration)
    end

    def process(line)
      puts line
      @execute_error = RakeExecuteException if line =~ /rake aborted/
      @rake_completed = true if line =~ /Tidy task done./
    end

    def execute(command)
      Bundler.with_clean_env do 
        begin
          tries ||= 3
          @execute_error = false
          @rake_completed = false
          puts "running: #{command}"
          
          Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| 
            process(line) while line = stdout.gets
            process(line) while line = stderr.gets
            
            raise RakeStatusException, "Non-zero Exit Code: #{wait_thr.value}" unless wait_thr.value.success?
            raise RakeCompletedException if (@rake_completed == false && command =~ /heroku run rake tidy:db:/ )
            raise @execute_error if @execute_error
          end
        rescue => e
          if (tries -= 1) > 0
            puts "Failed running command. Waiting 10s before next retry. Error: #{e}"
            sleep(10)
            retry
          else
            raise RuntimeError, "Error running command. Retried 3 times. Error #{e}"
          end
        end
      end
    end

    def app_name(stage)
      "#{configuration.app}-#{stage}"
    end

    def dyno_config
      YAML.load_file("#{Rails.root}/config/tidy/dynos.yml")
    end

    def app_stop(stage)
      execute("heroku maintenance:on --app #{app_name(stage)}")
      scale_command = dyno_config[stage].inject("") { |command, config| command += "#{config.first}='0' " }
      execute("heroku ps:scale #{scale_command} --app #{app_name(stage)}")
    end

    def app_start(stage)
      execute("heroku maintenance:off --app #{app_name(stage)}")
      scale_command = dyno_config[stage].inject("") { |command, config| command += "#{config.first}='#{config.last}' " }
      execute("heroku ps:scale #{scale_command} --app #{app_name(stage)}")
    end

    def master_connection_from_db_config(db_config)
      # Get connection to master/maintenance database 'postgres'
      pool = ActiveRecord::Base.send(establish_connection_method, db_config.merge(
              'database'           => 'postgres',
              'schema_search_path' => 'public'
            ))
      if pool.is_a?(Array)
        raise RuntimeError, "Pool an array size #{pool.size}. First: #{pool.first}"
      end
      pool.connection
    end

    def establish_connection_method
      # Choose proper connection method when ActiveRecord uses activerecord-import
      if ActiveRecord::Base.respond_to?(:establish_connection_without_activerecord_import)
        :establish_connection_without_activerecord_import
      else 
        :establish_connection
      end
    end

    def database_purge(db_config)
      encoding = configuration.database_encoding
      ActiveRecord::Base.remove_connection

      begin
        master_connection = master_connection_from_db_config(db_config)
        # Test connection
        master_connection.select_all("SELECT 1;")

        # Disallow connections from thinknear user. 
        master_connection.select_all("ALTER DATABASE #{db_config['database']} CONNECTION LIMIT 0;")

        # Terminate all connections to the database
        master_connection.select_all("SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE datname='#{db_config['database']}' AND pid <> pg_backend_pid();")

        # Drop the database
        master_connection.drop_database(db_config['database'])

        # Create the database
        master_connection.create_database(db_config['database'], db_config.merge('encoding' => encoding))

        # Enable connections from thinknear user
        master_connection.select_all("ALTER DATABASE #{db_config['database']} CONNECTION LIMIT -1;")
        
        # Connect to created database to set extensions
        ActiveRecord::Base.remove_connection
        ActiveRecord::Base.send(establish_connection_method, db_config).connection.execute("CREATE EXTENSION IF NOT EXISTS postgis")

      rescue ActiveRecord::StatementInvalid => error
        if /database .* already exists/ === error.message
          raise DatabaseAlreadyExists
        elsif /database .* is being accessed by other users/ === error.message
          result = ActiveRecord::Base.send(establish_connection_method, db_config).connection.select_all("SELECT * FROM pg_stat_activity;")
          puts result.to_hash
          raise
        else
          raise
        end
      ensure
        master_connection = master_connection_from_db_config(db_config)
        master_connection.select_all("ALTER DATABASE #{db_config['database']} CONNECTION LIMIT -1;")
      end
    end
  end
end