module Jennifer
  module Migration
    # This modules is responsible for processing database migration operations like creation,
    # dropping and migration.
    module Runner
      @@pending_versions = [] of String

      # Invokes migrations. *count* with negative or zero value will invoke all pending migrations.
      def self.migrate(count : Int = -1)
        performed = false
        default_adapter.ready_to_migrate!
        return unless pending_migration?

        assert_outdated_pending_migrations
        migrations = Base.migrations

        pending_versions.each_with_index do |version, i|
          return if count > 0 && i >= count

          process_up_migration(migrations[version].new)
          performed = true
        end
      ensure
        default_adapter.generate_schema if performed && !Config.skip_dumping_schema_sql
      end

      # Creates database using given *adapter*.
      #
      # If database already exists - do nothing.
      #
      # By default use application default adapter.
      def self.create(adapter : Adapter::Base = default_adapter)
        if adapter.database_exists?
          puts "#{adapter.config.db} is already exists"
        else
          adapter.create_database
          puts "#{adapter.config.db} is created!"
        end
      end

      # Drops database using given *adapter*.
      #
      # By default use application default adapter.
      def self.drop(adapter : Adapter::Base = default_adapter)
        adapter.drop_database
        puts "#{adapter.config.db} is dropped!"
      end

      # Rollbacks migrations.
      #
      # Allowed options:
      # - *count* - count of migrations to be rolled back
      # - *to* - migration timestamp to which database should be rolled back
      def self.rollback(options : Hash(Symbol, DBAny))
        processed = true
        default_adapter.ready_to_migrate!
        migrations = Base.migrations
        return if migrations.empty? || !Version.all.exists?

        versions =
          if options[:count]?
            Version.all.order(version: :desc).limit(options[:count].to_i).pluck(:version)
          elsif options[:to]?
            v = options[:to].to_s
            Version.all.order(version: :desc).where { _version > v }.pluck(:version)
          else
            raise ArgumentError.new
          end

        versions.each do |version|
          process_down_migration(migrations[version].new)
          processed = true
        end
      ensure
        default_adapter.generate_schema if processed && !Config.skip_dumping_schema_sql
      end

      # Loads schema from the SQL schema file.
      def self.load_schema
        return if Config.skip_dumping_schema_sql

        default_adapter.load_schema
        puts "Schema loaded"
      end

      # Returns whether pending migration exists.
      #
      # Pending migration - known Jennifer::Migration::Base subclasses that hasn't been run.
      def self.pending_migration?
        !pending_versions.empty?
      end

      private def self.default_adapter
        Adapter.default_adapter
      end

      # NOTE: pending versions are memorized so reloading should be performed manually.
      private def self.pending_versions
        @@pending_versions = (Base.versions - Version.list).sort! if @@pending_versions.empty?
        @@pending_versions
      end

      private def self.default_adapter_class
        Adapter.default_adapter_class
      end

      private def self.process_up_migration(migration)
        optional_transaction(migration) do
          process_with_announcement(migration, :up) do
            migration.up
            Version.create(version: migration.class.version)
          end
        end
      rescue e
        optional_transaction(migration) do
          if Config.instance.migration_failure_handler_method.reverse_direction?
            migration.down
          elsif Config.instance.migration_failure_handler_method.callback?
            migration.after_up_failure
          end
        end

        raise e
      end

      private def self.process_down_migration(migration)
        optional_transaction(migration) do
          process_with_announcement(migration, :down) do
            migration.down
            Version.all.where { _version == migration.class.version }.delete
          end
        end
      rescue e
        optional_transaction(migration) do
          if Config.instance.migration_failure_handler_method.reverse_direction?
            migration.up
          elsif Config.instance.migration_failure_handler_method.callback?
            migration.after_down_failure
          end
        end

        raise e
      end

      private def self.process_with_announcement(migration, direction)
        words =
          case direction
          when :up
            {start: "migrating", end: "migrated"}
          else
            {start: "reverting", end: "reverted"}
          end
        header = "== #{migration.class.version} #{migration.class}:"
        puts "#{header} #{words[:start]}" if Config.config.verbose_migrations
        time = Time.measure { yield }
        puts "#{header} #{words[:end]} (#{time.milliseconds} ms)\n" if Config.config.verbose_migrations
      end

      private def self.assert_outdated_pending_migrations
        return if !Version.all.exists? || Config.config.allow_outdated_pending_migration

        db_version = Version.all.order(version: :desc).first!.version
        broken = pending_versions.select { |version| version < db_version }
        return if broken.empty?

        raise <<-MESSAGE
          Can't run migrations because some of them are older then release version.
          They are:
          #{broken.map { |v| "- #{v}" }.join("\n")}
        MESSAGE
      end

      private def self.optional_transaction(migration)
        if migration.class.with_transaction?
          Model::Base.transaction { yield }
        else
          yield
        end
      end
    end
  end
end
