# frozen_string_literal: true

require "uri"
require "shellwords"
require "open3"

module Hanami
  module Model
    class Migrator
      # Migrator base adapter
      #
      # @since 0.4.0
      # @api private
      class Adapter
        # Migrations table to store migrations metadata.
        #
        # @since 0.4.0
        # @api private
        MIGRATIONS_TABLE = :schema_migrations

        # Migrations table version column
        #
        # @since 0.4.0
        # @api private
        MIGRATIONS_TABLE_VERSION_COLUMN = :filename

        # Loads and returns a specific adapter for the given connection.
        #
        # @since 0.4.0
        # @api private
        def self.for(configuration)
          connection = Connection.new(configuration)

          case connection.database_type
          when :sqlite
            require "hanami/model/migrator/sqlite_adapter"
            SQLiteAdapter
          when :postgres
            require "hanami/model/migrator/postgres_adapter"
            PostgresAdapter
          when :mysql
            require "hanami/model/migrator/mysql_adapter"
            MySQLAdapter
          else
            self
          end.new(connection)
        end

        # Initialize an adapter
        #
        # @since 0.4.0
        # @api private
        def initialize(connection)
          @connection = connection
        end

        # Create database.
        # It must be implemented by subclasses.
        #
        # @since 0.4.0
        # @api private
        #
        # @see Hanami::Model::Migrator.create
        def create
          raise MigrationError.new("Current adapter (#{connection.database_type}) doesn't support create.")
        end

        # Drop database.
        # It must be implemented by subclasses.
        #
        # @since 0.4.0
        # @api private
        #
        # @see Hanami::Model::Migrator.drop
        def drop
          raise MigrationError.new("Current adapter (#{connection.database_type}) doesn't support drop.")
        end

        # @since 0.4.0
        # @api private
        def migrate(migrations, version)
          version = Integer(version) unless version.nil?

          Sequel::Migrator.run(connection.raw, migrations, target: version, allow_missing_migration_files: true)
        rescue Sequel::Migrator::Error => exception
          raise MigrationError.new(exception.message)
        end

        # @since 1.1.0
        # @api private
        def rollback(migrations, steps)
          table = migrations_table_dataset
          version = version_to_rollback(table, steps)

          Sequel::Migrator.run(connection.raw, migrations, target: version, allow_missing_migration_files: true)
        rescue Sequel::Migrator::Error => exception
          raise MigrationError.new(exception.message)
        end

        # Load database schema.
        # It must be implemented by subclasses.
        #
        # @since 0.4.0
        # @api private
        #
        # @see Hanami::Model::Migrator.prepare
        def load
          raise MigrationError.new("Current adapter (#{connection.database_type}) doesn't support load.")
        end

        # Database version.
        #
        # @since 0.4.0
        # @api private
        def version
          table = migrations_table_dataset
          return if table.nil?

          record = table.order(MIGRATIONS_TABLE_VERSION_COLUMN).last
          return if record.nil?

          record.fetch(MIGRATIONS_TABLE_VERSION_COLUMN).scan(MIGRATIONS_FILE_NAME_PATTERN).first.to_s
        end

        private

        # @since 1.1.0
        # @api private
        MIGRATIONS_FILE_NAME_PATTERN = /\A[\d]{14}/.freeze

        # @since 1.1.0
        # @api private
        def version_to_rollback(table, steps)
          record = table.order(Sequel.desc(MIGRATIONS_TABLE_VERSION_COLUMN)).all[steps]
          return 0 unless record

          record.fetch(MIGRATIONS_TABLE_VERSION_COLUMN).scan(MIGRATIONS_FILE_NAME_PATTERN).first.to_i
        end

        # @since 1.1.0
        # @api private
        def migrations_table_dataset
          connection.table(MIGRATIONS_TABLE)
        end

        # @since 0.5.0
        # @api private
        attr_reader :connection

        # @since 0.4.0
        # @api private
        def schema
          connection.schema
        end

        # Returns a database connection
        #
        # Given a DB connection URI we can connect to a specific database or not, we need this when creating
        # or dropping a database. Important to notice that we can't always open a _global_ DB connection,
        # because most of the times application's DB user has no rights to do so.
        #
        # @param global [Boolean] determine whether or not a connection should specify a database.
        #
        # @since 0.5.0
        # @api private
        def new_connection(global: false)
          uri = global ? connection.global_uri : connection.uri

          Sequel.connect(uri)
        end

        # @since 0.4.0
        # @api private
        def database
          escape connection.database
        end

        # @since 0.4.0
        # @api private
        def port
          escape connection.port
        end

        # @since 0.4.0
        # @api private
        def host
          escape connection.host
        end

        # @since 0.4.0
        # @api private
        def username
          escape connection.user
        end

        # @since 0.4.0
        # @api private
        def password
          escape connection.password
        end

        # @since 0.4.0
        # @api private
        def migrations_table
          escape MIGRATIONS_TABLE
        end

        # @since 0.4.0
        # @api private
        def escape(string)
          Shellwords.escape(string) unless string.nil?
        end

        # @since 1.0.2
        # @api private
        def execute(command, env: {}, error: ->(err) { raise MigrationError.new(err) })
          Open3.popen3(env, command) do |_, stdout, stderr, wait_thr|
            error.call(stderr.read) unless wait_thr.value.success?
            yield stdout if block_given?
          end
        end
      end
    end
  end
end
