defmodule Ash.Resource.Calculation.Expression do
  @moduledoc false
  use Ash.Resource.Calculation

  def expression(opts, context) do
    expr =
      Ash.Expr.fill_template(
        opts[:expr],
        actor: context.actor,
        tenant: context.tenant,
        args: context.arguments,
        context: context.source_context
      )

    if type = context.type do
      case expr do
        %Ash.Query.Function.Type{arguments: [_, ^type | _]} ->
          expr

        %Ash.Query.Call{name: :type, args: [_, ^type | _]} ->
          expr

        _ ->
          {:ok, expr} =
            Ash.Query.Function.Type.new([
              expr,
              context.type,
              context.constraints || []
            ])

          expr
      end
    else
      expr
    end
  end

  def calculate([], _, _), do: []

  def calculate([%resource{} | _] = records, opts, context) do
    expression =
      Ash.Expr.fill_template(
        opts[:expr],
        actor: context.actor,
        tenant: context.tenant,
        args: context.arguments,
        context: context.source_context || %{}
      )

    Enum.reduce_while(records, {:ok, []}, fn record, {:ok, values} ->
      case Ash.Filter.hydrate_refs(expression, %{
             resource: resource,
             public?: false,
             eval?: true
           }) do
        {:ok, expression} ->
          case Ash.Expr.eval_hydrated(expression,
                 record: record,
                 resource: resource,
                 actor: context.actor,
                 unknown_on_unknown_refs?: true
               ) do
            {:ok, value} ->
              value = try_cast_stored(value, context.type, context.constraints)
              {:cont, {:ok, [value | values]}}

            :unknown ->
              {:halt, :unknown}

            {:error, error} ->
              {:halt, {:error, error}}
          end

        {:error, error} ->
          {:halt, {:error, error}}
      end
    end)
    |> case do
      {:ok, values} ->
        {:ok, Enum.reverse(values)}

      :unknown ->
        :unknown

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

  def try_cast_stored(value, nil, _constraints), do: value

  def try_cast_stored(value, type, constraints) do
    case Ash.Type.cast_stored(type, value, constraints || []) do
      {:ok, value} -> value
      _ -> value
    end
  end

  def load(query, opts, context) do
    expr =
      Ash.Expr.fill_template(
        opts[:expr],
        actor: context.actor,
        tenant: context.tenant,
        args: context.arguments,
        context: context.source_context || %{}
      )

    case Ash.Filter.hydrate_refs(expr, %{
           resource: query.resource,
           calculations: query.calculations,
           aggregates: query.aggregates,
           public?: false,
           eval?: true
         }) do
      {:ok, expression} ->
        expression
        |> Ash.Filter.list_refs()
        |> Enum.reduce([], fn
          %{attribute: %Ash.Query.Aggregate{} = agg, relationship_path: relationship_path}, acc ->
            add_at_path(acc, relationship_path, agg)

          %{attribute: %Ash.Query.Calculation{} = calc, relationship_path: relationship_path},
          acc ->
            add_at_path(acc, relationship_path, calc)

          %{attribute: %{name: name}, relationship_path: relationship_path}, acc ->
            add_at_path(acc, relationship_path, name)
        end)
        |> Enum.concat(Ash.Filter.used_aggregates(expression))
        |> Enum.uniq()

      {:error, _} ->
        []
    end
  end

  defp add_at_path(acc, [], value) do
    Enum.uniq([value | acc])
  end

  defp add_at_path(acc, [first | rest], value) do
    Enum.reduce(acc, {acc, false}, fn
      {key, current}, {acc, _found?} when key == first ->
        {[{key, add_at_path(List.wrap(current), rest, value)} | acc], true}

      key, {acc, _found?} when key == first ->
        {[{key, add_at_path([], rest, value)} | acc], true}

      v, {acc, found?} ->
        {[v | acc], found?}
    end)
    |> case do
      {acc, false} ->
        [{first, add_at_path([], rest, value)} | acc]

      {acc, _} ->
        acc
    end
  end
end
