defmodule Mix.Tasks.Whatwasit.Install do
  @moduledoc """
  Setup the Whatwasit package for your Phoenix application.

  Adds a migration for the Version model used to trackage changes to
  the desired models.

  Prints example configuration that should be added to your
  config/config.exs file.

  ## Examples

      # create the migration and print configuration
      mix whatwasit.install

      # print configuration
      mix whatwasit.install --no-migrations

      # Add current user tracking
      mix whatwasit.install --whodoneit

      # use a different user model
      mix whatwasit.install --whodoneit --model="Account accounts"

      # use a simple whodoneit_id field
      mix whatwasit.install --whodoneit-id=integer

      # store the whodoneit model in a map
      mix whatwasit.install --whodoneit-map

      # set the whodoneit model foreign key type to uuid
      mix whatwasit.install --whodoneit-id-type=uuid

  The following options are available:

  * `--model` -- The authentication model and table_name
  * `--repo` -- The project's repo if different than the standard default
  * `--module` -- The projects base module
  * `--migration-path` -- The migration path
  * `--whodoneit` -- Add current user tracking
  * `--whodoneit-id` -- Use a simple id field instead of a relationship
  * `--whodoneit-map` -- Store the current_user as a map
  * `--whodoneit-id-type` -- Set the foreign key type

  The following options are available to disable features:

  * `--no-migrations` -- Don't generate the migration
  * `--no-boilerplate` -- Don't generate anything
  * `--no-models` -- Dont' generate models
  """

  @shortdoc "Configure the Whatwasit Package"

  import Macro, only: [camelize: 1, underscore: 1]
  import Mix.Generator
  import Mix.Ecto
  import Whatwasit.Mix.Utils

  @default_options ~w()
  # the options that default to true, and can be disabled with --no-option
  @default_booleans  ~w(config migrations boilerplate models)

  # all boolean_options
  @boolean_options   @default_booleans

  @switches [repo: :string, migration_path: :string, model: :string, module: :string, whodoneit: :boolean, whodoneit_id: :string, whodoneit_map: :boolean, whodoneit_id_type: :string] ++ Enum.map(@boolean_options, &({String.to_atom(&1), :boolean}))
  @switch_names Enum.map(@switches, &(elem(&1, 0)))


  def run(args) do
    {opts, parsed, unknown} = OptionParser.parse(args, switches: @switches)

    verify_args!(parsed, unknown)

    {bin_opts, opts} = parse_options(opts)

    do_config(opts, bin_opts)
    |> do_run
  end

  def do_run(config) do
    config
    |> gen_migration
    |> gen_version_model
    |> print_instructions
  end

  defp gen_version_model(%{models: true, whodoneit_map: true, boilerplate: true, binding: binding} = config) do
    changeset_fields = "~w(item_type item_id object action whodoneit)a"
    schema_fields = "field :whodoneit, :map\n"
    binding = binding ++ [
      schema_fields: schema_fields,
      changeset_fields: changeset_fields
    ]
    Mix.Phoenix.copy_from paths(),
      "priv/templates/whatwasit.install/models/whatwasit", "", binding, [
        {:eex, "version_map.ex", "web/models/whatwasit/version.ex"},
      ]
    config
  end

  defp gen_version_model(%{models: true, boilerplate: true, binding: binding} = config) do
    name_field = "field :whodoneit_name, :string\n"
    changeset_fields = "item_type item_id object action"
    whodoneit_changeset_fields = " whodoneit_id whodoneit_name"
    whodoneit_id = config[:whodoneit_id]
    whodoneit = config[:whodoneit]
    {schema_fields, add_changeset_fields} = cond do
      config[:whodoneit_id_type] ->
        {name_field <> "    belongs_to :whodoneit, #{config[:user_schema] |> elem(0)}, type: Ecto.UUID\n", whodoneit_changeset_fields}
      whodoneit_id ->
        {name_field <> "    field :whodoneit_id, :#{whodoneit_id}\n", whodoneit_changeset_fields}
      whodoneit ->
        {name_field <> "    belongs_to :whodoneit, #{config[:user_schema] |> elem(0)}\n", whodoneit_changeset_fields}
      true ->
        {"", ""}
    end
    binding = binding ++ [
      schema_fields: schema_fields,
      changeset_fields: "~w(#{changeset_fields}#{add_changeset_fields})a"
    ]
    Mix.Phoenix.copy_from paths(),
      "priv/templates/whatwasit.install/models/whatwasit", "", binding, [
        {:eex, "version.ex", "web/models/whatwasit/version.ex"},
      ]
    config
  end
  defp gen_version_model(config) do
    config
  end

  defp gen_migration(%{migrations: true, boilerplate: true} = config) do
    {_, table_name} = config[:user_schema]
    whodoneit = cond do
      config[:whodoneit_id_type] ->
        """
              add :whodoneit_name, :string
              add :whodoneit_id, references(:#{table_name}, on_delete: :nilify_all, type: :uuid)
        """
      config[:whodoneit_map] ->
        """
              add :whodoneit, :map
        """
      config[:whodoneit_id] ->
        """
              add :whodoneit_name, :string
              add :whodoneit_id, :#{config[:whodoneit_id]}
        """
      config[:whodoneit] ->
        """
              add :whodoneit_name, :string
              add :whodoneit_id, references(:#{table_name}, on_delete: :nilify_all)
        """
      true -> ""
    end
    do_gen_migration config, "create_whatwasit_version", fn repo, _path, file, name ->

      change = """
          create table(:versions) do
            add :item_type, :string, null: false
            add :item_id, :integer, null: false
            add :action, :string
            add :object, :map, null: false
      """ <> whodoneit <> """
            timestamps
          end
      """
      assigns = [mod: Module.concat([repo, Migrations, camelize(name)]),
                       change: change]
      create_file file, migration_template(assigns)
    end
  end
  defp gen_migration(config), do: config

  defp do_gen_migration(config, name, fun) do
    timestamp = timestamp()
    repo = config[:repo]
    |> String.split(".")
    |> Module.concat
    ensure_repo(repo, [])
    path = case config[:migration_path] do
      path when is_binary(path) -> path
      _ ->
        Path.relative_to(migrations_path(repo), Mix.Project.app_path)
    end
    file = Path.join(path, "#{timestamp}_#{underscore(name)}.exs")
    fun.(repo, path, file, name)
    config
  end

  defp print_instructions(%{whodoneit_id: type} = config) when type != nil do
    Mix.shell.info """
    Add the following to your config/config.exs:

      config :whatwasit,
        repo: #{config[:repo]},
        whodoneit_id: :#{type}

    """ <> default_model_instructions(config)
    config
  end
  defp print_instructions(%{whodoneit: true, base: base} = config) do
    Mix.shell.info """
    Add the following to your config/config.exs:

      config :whatwasit,
        repo: #{config[:repo]}


    Update your models so the look like this:

      defmodule #{base}.Post do
        use #{base}.Web, :model
        use Whatwasit         # add this

        schema "posts" do
          field :title, :string
          field :body, :string
          timestamps
        end

        def changeset(model, params \\ %{}, opts \\ []) do
          model
          |> cast(params, ~w(title body))
          |> validate_required(~w(title body)a)
          |> prepare_version(opts)   # add this
        end
      end
    """
    config
  end
  defp print_instructions(config) do
    Mix.shell.info """
    Add the following to your config/config.exs:

      config :whatwasit,
        repo: #{config[:repo]}

    """ <> default_model_instructions(config)
    config
  end
  defp default_model_instructions(%{base: base}) do
    """
    Update your models like this:

      defmodule #{base}.Post do
        use #{base}.Web, :model
        use Whatwasit         # add this

        schema "posts" do
          field :title, :string
          field :body, :string
          timestamps
        end

        def changeset(model, params \\ %{}) do
          model
          |> cast(params, ~w(title body))
          |> validate_required(~w(title body)a)
          |> prepare_version     # add this
        end
      end

    """
  end

  defp timestamp do
    {{y, m, d}, {hh, mm, ss}} = :calendar.universal_time()
    "#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
  end

  embed_template :migration, """
  defmodule <%= inspect @mod %> do
    use Ecto.Migration
    def change do
  <%= @change %>
    end
  end
  """

  #############
  # Config

  defp do_config(opts, bin_opts) do
    binding = Mix.Project.config
    |> Keyword.fetch!(:app)
    |> Atom.to_string
    |> Mix.Phoenix.inflect

    # IO.puts "binding: #{inspect binding}"

    base = opts[:module] || binding[:base]
    opts = Keyword.put(opts, :base, base)
    repo = (opts[:repo] || "#{base}.Repo")

    binding = Keyword.put binding ,:base, base

    user_schema = parse_model(opts[:model], base, opts)

    whodoneit = if opts[:whodoneit_map] || opts[:whodoneit_id] || opts[:whodoneit_id_type], do: true, else: opts[:whodoneit]

    case opts[:whodoneit_id_type] do
      false -> :ok
      nil -> :ok
      "uuid" -> :ok
      other ->
        IO.puts "other: #{inspect other}"
        Mix.raise """
        --whodoneit-id-type only supports option uuid
        """
    end

    bin_opts
    |> Enum.map(&({&1, true}))
    |> Enum.into(%{})
    |> Map.put(:base, base)
    |> Map.put(:user_schema, user_schema)
    |> Map.put(:repo, repo)
    |> Map.put(:binding, binding)
    |> Map.put(:migration_path, opts[:migration_path])
    |> Map.put(:module, opts[:module])
    |> Map.put(:whodoneit, whodoneit)
    |> Map.put(:whodoneit_id, opts[:whodoneit_id])
    |> Map.put(:whodoneit_map, opts[:whodoneit_map])
    |> Map.put(:whodoneit_id_type, opts[:whodoneit_id_type])
    |> do_default_config(opts)
  end

  defp parse_options(opts) do
    {opts_bin, opts} = Enum.reduce opts, {[], []}, fn
      opt, {acc_bin, acc} ->
        {acc_bin, [opt | acc]}
    end
    opts_bin = Enum.uniq(opts_bin)
    opts_names = Enum.map opts, &(elem(&1, 0))
    with  [] <- Enum.filter(opts_bin, &(not &1 in @switch_names)),
          [] <- Enum.filter(opts_names, &(not &1 in @switch_names)) do
            {opts_bin, opts}
    else
      list -> raise_option_errors(list)
    end
  end

  ################
  # Utilities

  defp pad(i) when i < 10, do: << ?0, ?0 + i >>
  defp pad(i), do: to_string(i)

  defp do_default_config(config, opts) do
    list_to_atoms(@default_booleans)
    |> Enum.reduce(config, fn opt, acc ->
      Map.put acc, opt, Keyword.get(opts, opt, true)
    end)
  end

  defp list_to_atoms(list), do: Enum.map(list, &(String.to_atom(&1)))

  defp parse_model(model, _base, opts) when is_binary(model) do
    case String.split(model, " ", trim: true) do
      [model, table] ->
        {prefix_model(model, opts), String.to_atom(table)}
      [_] ->
        Mix.raise """
        The mix whatwasit.install --model option expects both singular and plural names. For example:

            mix whatwasit.install --model="Account accounts"
        """
    end
  end
  defp parse_model(_, base, _) do
    {"#{base}.User", :users}
  end

  defp prefix_model(model, opts) do
    module = opts[:module] || opts[:base]
    if String.starts_with? model, module do
      model
    else
      module <> "." <>  model
    end
  end

  defp paths do
    [".", :whatwasit]
  end
end
