defmodule Mix.Shopifex.Migration do
  @moduledoc """
  Utilities module for ecto migrations in mix tasks.
  """
  alias Mix.Generator

  @doc """
  Creates a migration file for a repo.
  """
  @spec create_migration_file(atom(), binary(), binary()) :: any()
  def create_migration_file(repo, name, content) do
    base_name = "#{Macro.underscore(name)}.exs"

    path =
      repo
      |> Mix.EctoSQL.source_repo_priv()
      |> Path.join("migrations")
      |> maybe_create_directory()

    timestamp = timestamp(path)

    path
    |> ensure_unique(base_name, name)
    |> Path.join("#{timestamp}_#{base_name}")
    |> Generator.create_file(content)
  end

  defp maybe_create_directory(path) do
    Generator.create_directory(path)

    path
  end

  defp ensure_unique(path, base_name, name) do
    path
    |> Path.join("*_#{base_name}")
    |> Path.wildcard()
    |> case do
      [] ->
        path

      _ ->
        Mix.raise(
          "migration can't be created, there is already a migration file with name #{name}."
        )
    end
  end

  defp timestamp(path, seconds \\ 0) do
    timestamp = gen_timestamp(seconds)

    path
    |> Path.join("#{timestamp}_*.exs")
    |> Path.wildcard()
    |> case do
      [] -> timestamp
      _ -> timestamp(path, seconds + 1)
    end
  end

  defp gen_timestamp(seconds) do
    %{year: y, month: m, day: d, hour: hh, minute: mm, second: ss} =
      DateTime.utc_now()
      |> DateTime.to_unix()
      |> Kernel.+(seconds)
      |> DateTime.from_unix!()

    "#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
  end

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

  @template """
  defmodule <%= inspect migration.repo %>.Migrations.<%= migration.name %> do
    use Ecto.Migration
    def change do
  <%= for schema <- migration.schemas do %>
      create table(:<%= schema.table %><%= if schema.binary_id do %>, primary_key: false<% end %>) do
  <%= if schema.binary_id do %>      add :id, :binary_id, primary_key: true
  <% end %><%= for {k, v} <- schema.attrs do %>      add <%= inspect k %>, <%= inspect v %><%= schema.defaults[k] %>
  <% end %><%= for {_, i, _, s} <- schema.assocs do %>      add <%= if(String.ends_with?(inspect(i), "_id"), do: inspect(i), else: inspect(i) <> "_id") %>, references(<%= inspect(s) %>, on_delete: :nothing<%= if schema.binary_id do %>, type: :binary_id<% end %>)
  <% end %>
        timestamps()
      end
  <%= for index <- schema.indexes do %>
      <%= index %><% end %>
  <% end %>
    end
  end
  """

  alias Mix.Shopifex.{Shop, Plan, Grant}

  @schemas [
    {"shops", Shop},
    {"plans", Plan},
    {"grants", Grant}
  ]

  @spec gen(binary(), binary(), map()) :: binary()
  def gen(name, namespace, %{repo: repo} = config) do
    schemas =
      for {table, module} <- @schemas,
          do: schema(module, table, namespace, config)

    EEx.eval_string(@template, migration: %{repo: repo, name: name, schemas: schemas})
  end

  defp schema(module, table, namespace, %{binary_id: binary_id}) do
    attrs =
      module.attrs()
      |> Kernel.++(attrs_from_assocs(module.assocs(), namespace))
      |> migration_attrs()

    defaults = defaults(attrs)
    {assocs, attrs} = partition_attrs(attrs)
    table = "#{namespace}_#{table}"
    indexes = migration_indexes(module.indexes(), table)

    %{
      table: table,
      binary_id: binary_id,
      attrs: attrs,
      defaults: defaults,
      assocs: assocs,
      indexes: indexes
    }
  end

  def attrs_from_assocs(assocs, namespace) do
    assocs
    |> Enum.map(&attr_from_assoc(&1, namespace))
    |> Enum.reject(&is_nil/1)
  end

  defp attr_from_assoc({:belongs_to, name, :users}, _namespace) do
    {String.to_atom("#{name}_id"), {:references, :users}}
  end

  defp attr_from_assoc({:belongs_to, name, table}, namespace) do
    {String.to_atom("#{name}_id"), {:references, String.to_atom("#{namespace}_#{table}")}}
  end

  defp attr_from_assoc({:belongs_to, name, table, _defaults}, namespace),
    do: attr_from_assoc({:belongs_to, name, table}, namespace)

  defp attr_from_assoc(_assoc, _opts), do: nil

  defp migration_attrs(attrs) do
    Enum.map(attrs, &to_migration_attr/1)
  end

  defp to_migration_attr({name, type}) do
    {name, type, ""}
  end

  defp to_migration_attr({name, type, []}) do
    to_migration_attr({name, type})
  end

  defp to_migration_attr({name, type, defaults}) do
    defaults = Enum.map_join(defaults, ", ", fn {k, v} -> "#{k}: #{inspect(v)}" end)

    {name, type, ", #{defaults}"}
  end

  defp defaults(attrs) do
    Enum.map(attrs, fn {key, _value, defaults} ->
      {key, defaults}
    end)
  end

  defp partition_attrs(attrs) do
    {assocs, attrs} =
      Enum.split_with(attrs, fn
        {_, {:references, _}, _} -> true
        _ -> false
      end)

    attrs = Enum.map(attrs, fn {key_id, type, _defaults} -> {key_id, type} end)

    assocs =
      Enum.map(assocs, fn {key_id, {:references, source}, _} ->
        key = String.replace(Atom.to_string(key_id), "_id", "")
        {String.to_atom(key), key_id, nil, source}
      end)

    {assocs, attrs}
  end

  defp migration_indexes(indexes, table) do
    Enum.map(indexes, &to_migration_index(table, &1))
  end

  defp to_migration_index(table, {key_or_keys, true}),
    do: "create unique_index(:#{table}, #{inspect(List.wrap(key_or_keys))})"

  defp to_migration_index(table, {key_or_keys, :gin}),
    do: "create index(:#{table}, #{inspect(List.wrap(key_or_keys))}, using: \"GIN\")"
end
