require 'term/ansicolor'
require 'uri'

module Aptible
  module CLI
    module Subcommands
      module DB
        def self.included(thor)
          thor.class_eval do
            include Helpers::Operation
            include Helpers::Database
            include Helpers::Token
            include Term::ANSIColor

            desc 'db:list', 'List all databases'
            option :environment
            define_method 'db:list' do
              Formatter.render(Renderer.current) do |root|
                root.grouped_keyed_list(
                  { 'environment' => 'handle' },
                  'handle'
                ) do |node|
                  scoped_environments(options).each do |account|
                    account.each_database do |db|
                      node.object do |n|
                        ResourceFormatter.inject_database(n, db, account)
                      end
                    end
                  end
                end
              end
            end

            desc 'db:versions', 'List available database versions'
            define_method 'db:versions' do
              Formatter.render(Renderer.current) do |root|
                root.grouped_keyed_list('type', 'version') do |node|
                  Aptible::Api::DatabaseImage.all(
                    token: fetch_token
                  ).each do |database_image|
                    node.object do |n|
                      n.value('type', database_image.type)
                      n.value('version', database_image.version)
                      n.value('default', database_image.default)
                      n.value('description', database_image.description)
                      n.value('docker_repo', database_image.default)
                    end
                  end
                end
              end
            end

            desc 'db:create HANDLE ' \
                 '[--type TYPE] [--version VERSION] ' \
                 '[--container-size SIZE_MB] [--size SIZE_GB]',
                 'Create a new database'
            option :type, type: :string
            option :version, type: :string
            option :container_size, type: :numeric
            option :size, default: 10, type: :numeric
            option :environment
            define_method 'db:create' do |handle|
              account = ensure_environment(options)

              db_opts = {
                handle: handle,
                initial_container_size: options[:container_size],
                initial_disk_size: options[:size]
              }.delete_if { |_, v| v.nil? }

              type = options[:type]
              version = options[:version]

              if version && type
                image = find_database_image(type, version)
                db_opts[:type] = image.type
                db_opts[:database_image] = image
              elsif version
                raise Thor::Error, '--type is required when passing --version'
              else
                db_opts[:type] = type || 'postgresql'
              end

              database = account.create_database!(db_opts)

              op_opts = {
                type: 'provision',
                container_size: options[:container_size],
                disk_size: options[:size]
              }.delete_if { |_, v| v.nil? }
              op = database.create_operation(op_opts)

              if op.errors.any?
                # NOTE: If we fail to provision the database, we should try and
                # clean it up immediately. Note that this will not be possible
                # if we have an account that's not activated, but that's
                # arguably the desired UX here.
                database.create_operation!(type: 'deprovision')
                raise Thor::Error, op.errors.full_messages.first
              end

              attach_to_operation_logs(op)

              render_database(database.reload, account)
            end

            desc 'db:clone SOURCE DEST', 'Clone a database to create a new one'
            option :environment
            define_method 'db:clone' do |source_handle, dest_handle|
              # TODO: Deprecate + recommend backup
              source = ensure_database(options.merge(db: source_handle))
              database = clone_database(source, dest_handle)
              render_database(database, database.account)
            end

            desc 'db:dump HANDLE', 'Dump a remote database to file'
            option :environment
            define_method 'db:dump' do |handle|
              database = ensure_database(options.merge(db: handle))
              with_postgres_tunnel(database) do |url|
                filename = "#{handle}.dump"
                CLI.logger.info "Dumping to #{filename}"
                `pg_dump #{url} > #{filename}`
              end
            end

            desc 'db:execute HANDLE SQL_FILE', 'Executes sql against a database'
            option :environment
            define_method 'db:execute' do |handle, sql_path|
              database = ensure_database(options.merge(db: handle))
              with_postgres_tunnel(database) do |url|
                CLI.logger.info "Executing #{sql_path} against #{handle}"
                `psql #{url} < #{sql_path}`
              end
            end

            desc 'db:tunnel HANDLE', 'Create a local tunnel to a database'
            option :environment
            option :port, type: :numeric
            option :type, type: :string
            define_method 'db:tunnel' do |handle|
              desired_port = Integer(options[:port] || 0)
              database = ensure_database(options.merge(db: handle))

              credential = find_credential(database, options[:type])

              m = "Creating #{credential.type} tunnel to #{database.handle}..."
              CLI.logger.info m

              if options[:type].nil?
                types = database.database_credentials.map(&:type)
                unless types.empty?
                  valid = types.join(', ')
                  CLI.logger.info 'Use --type TYPE to specify a tunnel type'
                  CLI.logger.info "Valid types for #{database.handle}: #{valid}"
                end
              end

              with_local_tunnel(credential, desired_port) do |tunnel_helper|
                port = tunnel_helper.port
                CLI.logger.info "Connect at #{local_url(credential, port)}"

                uri = URI(local_url(credential, port))
                db = uri.path.gsub(%r{^/}, '')
                CLI.logger.info 'Or, use the following arguments:'
                CLI.logger.info "* Host: #{uri.host}"
                CLI.logger.info "* Port: #{uri.port}"
                CLI.logger.info "* Username: #{uri.user}" unless uri.user.empty?
                CLI.logger.info "* Password: #{uri.password}"
                CLI.logger.info "* Database: #{db}" unless db.empty?

                CLI.logger.info 'Connected. Ctrl-C to close connection.'

                begin
                  tunnel_helper.wait
                rescue Interrupt
                  CLI.logger.warn 'Closing tunnel'
                end
              end
            end

            desc 'db:deprovision HANDLE', 'Deprovision a database'
            option :environment
            define_method 'db:deprovision' do |handle|
              database = ensure_database(options.merge(db: handle))
              CLI.logger.info "Deprovisioning #{database.handle}..."
              database.create_operation!(type: 'deprovision')
            end

            desc 'db:backup HANDLE', 'Backup a database'
            option :environment
            define_method 'db:backup' do |handle|
              database = ensure_database(options.merge(db: handle))
              CLI.logger.info "Backing up #{database.handle}..."
              op = database.create_operation!(type: 'backup')
              attach_to_operation_logs(op)
            end

            desc 'db:reload HANDLE', 'Reload a database'
            option :environment
            define_method 'db:reload' do |handle|
              database = ensure_database(options.merge(db: handle))
              CLI.logger.info "Reloading #{database.handle}..."
              op = database.create_operation!(type: 'reload')
              attach_to_operation_logs(op)
            end

            desc 'db:restart HANDLE ' \
                 '[--container-size SIZE_MB] [--size SIZE_GB]',
                 'Restart a database'
            option :environment
            option :container_size, type: :numeric
            option :size, type: :numeric
            define_method 'db:restart' do |handle|
              database = ensure_database(options.merge(db: handle))

              opts = {
                type: 'restart',
                container_size: options[:container_size],
                disk_size: options[:size]
              }.delete_if { |_, v| v.nil? }

              CLI.logger.info "Restarting #{database.handle}..."
              op = database.create_operation!(opts)
              attach_to_operation_logs(op)
            end

            desc 'db:url HANDLE', 'Display a database URL'
            option :environment
            option :type, type: :string
            define_method 'db:url' do |handle|
              database = ensure_database(options.merge(db: handle))
              credential = find_credential(database, options[:type])

              Formatter.render(Renderer.current) do |root|
                root.keyed_object('connection_url') do |node|
                  node.value('connection_url', credential.connection_url)
                end
              end
            end
          end
        end
      end
    end
  end
end