defmodule Mv.Release do
  @moduledoc """
  Used for executing DB release tasks when run in production without Mix
  installed.

  ## Tasks

  - `migrate/0` - Runs all pending Ecto migrations.
  - `seed_admin/0` - Ensures an admin user exists from ENV (ADMIN_EMAIL, ADMIN_PASSWORD
    or ADMIN_PASSWORD_FILE). Idempotent; can be run on every deployment or via shell
    to update the admin password without redeploying.
  """
  @app :mv

  alias Mv.Accounts
  alias Mv.Accounts.User
  alias Mv.Authorization.Role

  require Ash.Query

  def migrate do
    load_app()

    for repo <- repos() do
      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
    end
  end

  def rollback(repo, version) do
    load_app()
    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
  end

  @doc """
  Ensures an admin user exists from ENV (ADMIN_EMAIL, ADMIN_PASSWORD or ADMIN_PASSWORD_FILE).

  Starts the application if not already running (required when called via `bin/mv eval`;
  Ash/Telemetry need the running app). Idempotent.

  - If ADMIN_EMAIL is unset: no-op (idempotent).
  - If ADMIN_PASSWORD (and ADMIN_PASSWORD_FILE) are unset and the user does not exist:
    no user is created (no fallback password in production).
  - If both ADMIN_EMAIL and ADMIN_PASSWORD are set: creates or updates the user with
    Admin role and the given password. Safe to run on every deployment or via
    `bin/mv eval "Mv.Release.seed_admin()"` to change the admin password without redeploying.
  """
  def seed_admin do
    # Ensure app (and Telemetry/Ash deps) are started when run via bin/mv eval
    case Application.ensure_all_started(@app) do
      {:ok, _} -> :ok
      {:error, {app, reason}} -> raise "Failed to start #{inspect(app)}: #{inspect(reason)}"
    end

    admin_email = get_env("ADMIN_EMAIL", nil)
    admin_password = get_env_or_file("ADMIN_PASSWORD", nil)

    cond do
      is_nil(admin_email) or admin_email == "" ->
        :ok

      is_nil(admin_password) or admin_password == "" ->
        ensure_admin_role_only(admin_email)

      true ->
        ensure_admin_user(admin_email, admin_password)
    end
  end

  defp ensure_admin_role_only(email) do
    case Role.get_admin_role() do
      {:ok, nil} ->
        :ok

      {:ok, %Role{} = admin_role} ->
        case get_user_by_email(email) do
          {:ok, %User{} = user} ->
            user
            |> Ash.Changeset.for_update(:update, %{})
            |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
            |> Ash.update!(authorize?: false)

            :ok

          _ ->
            :ok
        end

      {:error, _} ->
        :ok
    end
  end

  defp ensure_admin_user(email, password) do
    if is_nil(password) or password == "" do
      :ok
    else
      do_ensure_admin_user(email, password)
    end
  end

  defp do_ensure_admin_user(email, password) do
    case Role.get_admin_role() do
      {:ok, nil} ->
        # Admin role does not exist (e.g. migrations not run); skip
        :ok

      {:ok, %Role{} = admin_role} ->
        case get_user_by_email(email) do
          {:ok, nil} ->
            create_admin_user(email, password, admin_role)

          {:ok, user} ->
            update_admin_user(user, password, admin_role)

          {:error, _} ->
            :ok
        end

      {:error, _} ->
        :ok
    end
  end

  defp create_admin_user(email, password, admin_role) do
    case Accounts.create_user(%{email: email}, authorize?: false) do
      {:ok, user} ->
        user
        |> Ash.Changeset.for_update(:admin_set_password, %{password: password})
        |> Ash.update!(authorize?: false)
        |> then(fn u ->
          u
          |> Ash.Changeset.for_update(:update, %{})
          |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
          |> Ash.update!(authorize?: false)
        end)

        :ok

      {:error, _} ->
        :ok
    end
  end

  defp update_admin_user(user, password, admin_role) do
    user
    |> Ash.Changeset.for_update(:admin_set_password, %{password: password})
    |> Ash.update!(authorize?: false)
    |> then(fn u ->
      u
      |> Ash.Changeset.for_update(:update, %{})
      |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
      |> Ash.update!(authorize?: false)
    end)

    :ok
  end

  defp get_user_by_email(email) do
    User
    |> Ash.Query.filter(email == ^email)
    |> Ash.read_one(authorize?: false, domain: Mv.Accounts)
  end

  defp get_env(key, default) do
    System.get_env(key, default)
  end

  defp get_env_or_file(var_name, default) do
    file_var = "#{var_name}_FILE"

    case System.get_env(file_var) do
      nil ->
        System.get_env(var_name, default)

      file_path ->
        case File.read(file_path) do
          {:ok, content} ->
            String.trim_trailing(content)

          {:error, _} ->
            default
        end
    end
  end

  defp repos do
    Application.fetch_env!(@app, :ecto_repos)
  end

  defp load_app do
    Application.load(@app)
  end
end
