defmodule Mix.Tasks.Cloak.Migrate.Ecto do
  @moduledoc """
  Migrates a schema table to a new encryption cipher.

  ## Rationale

  Cloak vaults will automatically decrypt fields which were encrypted
  by a retired key, and reencrypt them with the new key when they change.

  However, this usually is not enough for key rotation. Usually, you want
  to proactively reencrypt all your fields with the new key, so that the
  old key can be decommissioned.

  This task allows you to do just that.

  ## Strategy

  This task will migrate a table following this strategy:

  - Page through the table using a cursor, selecting primary key values. By
    default, the cursor is based on the schema's primary key. To use custom
    fields in the cursor, see `Cloak.CustomCursor`.

    Here's an example SQL query generated by `mix cloak.migrate.ecto`:

      ```sql
      SELECT primary_key
      FROM table_name
      WHERE primary_key > cursor
      ORDER BY primary_key ASC
      LIMIT 100
      ```

  - For each primary key value, in a database transaction,
    - Fetch the row with that key value, lock with "FOR UPDATE"
    - Reencrypt all Cloak fields with the new cipher
    - Write the row, unlocking it

  Update queries are issued in parallel to maximize speed. Each row is fetched
  and written back as quickly as possible to reduce the amount of time the
  row is locked.

  ## Warnings

  1. `mix cloak.migrate.ecto` works well with primary keys of the following
      types. If your key is not of these types, see `Cloak.CustomCursor`.

     - Integers
     - PostgreSQL UUID
     - MongoDB ObjectID

  2. Because `mix cloak.migrate.ecto` issues queries in parallel, it can consume
     all your database connections. For this reason, you may wish to use a
     separate `Repo` with a limited `:pool` just for Cloak migrations. This will
     allow you to prevent any performance impact by throttling Cloak to use only
     a limited number of database connections.

  ## Configuration

  Ensure that you have configured your vault to use the new cipher by default!

      # If using mix configuration...

      config :my_app, MyApp.Vault,
        ciphers: [
          default: {Cloak.Ciphers.AES.GCM, tag: "NEW", key: <<...>>},
          retired: {Cloak.Ciphers.AES.CTR, tag: "OLD", key: <<...>>>}
        ]

      # If configuring in the `init/1` callback:

      defmodule MyApp.Vault do
        use Cloak.Vault, otp_app: :my_app

        @impl Cloak.Vault
        def init(config) do
          config =
            Keyword.put(config, :ciphers, [
              default: {Cloak.Ciphers.AES.GCM, tag: "NEW", key: <<...>>},
              retired: {Cloak.Ciphers.AES.CTR, tag: "OLD", key: <<...>>>}
            ])

          {:ok, config}
        end
      end

  If you want to migrate multiple schemas at once, you may find it convenient
  to specify the schemas in your `config/config.exs`:

      config :my_app,
        cloak_repo: [MyApp.Repo],
        cloak_schemas: [MyApp.Schema1, MyApp.Schema2]

  ## Usage

  To run against only a specific repo and schema, use the `-r` and `-s` flags:

      mix cloak.migrate.ecto -r MyApp.Repo -s MyApp.Schema

  If you've configured multiple schemas at once, as shown above, you can simply
  run:

      mix cloak.migrate.ecto
  """

  use Mix.Task

  import IO.ANSI, only: [yellow: 0, green: 0, reset: 0]

  alias Cloak.Ecto.Migrator

  @doc false
  def run(args) do
    Mix.Task.run("app.start", [])
    configs = Mix.Cloak.Ecto.parse_config(args)

    for {_app, config} <- configs,
        schema <- config.schemas do
      Mix.shell().info("Migrating #{yellow()}#{inspect(schema)}#{reset()}...")
      Migrator.migrate(config.repo, schema)
      Mix.shell().info(green() <> "Migration complete!" <> reset())
    end

    :ok
  end
end
