defmodule AshCommanded.Commanded.Transformers.GenerateProjectionModules do
  @moduledoc """
  Generates projection modules based on the projections defined in the DSL.
  
  For each projection defined in a resource, this transformer will generate a corresponding module
  that handles the transformation of events into resource updates.
  
  This transformer should run after the event module transformer.
  
  ## Example
  
  Given a resource with the following projection:
  
  ```elixir
  projection :user_registered do
    action :create
    changes(%{status: :active})
  end
  ```
  
  This transformer will generate a module like:
  
  ```elixir
  defmodule MyApp.Projections.UserRegistered do
    @moduledoc "Projection that handles user_registered events"
    
    def apply(event, resource) do
      # Apply changes to the resource based on the event
      changes = %{status: :active}
      Ash.Changeset.for_action(resource, event, :create, changes)
    end
  end
  ```
  """
  
  use Spark.Dsl.Transformer
  alias Spark.Dsl.Transformer
  alias AshCommanded.Commanded.Transformers.BaseTransformer
  alias AshCommanded.Commanded.Transformers.GenerateEventModules
  
  @doc """
  Specifies that this transformer should run after the event module transformer.
  """
  @impl true
  def after?(GenerateEventModules), do: true
  def after?(_), do: false
  
  @doc """
  Transforms the DSL state to generate projection modules.
  
  ## Examples
  
      iex> transform(dsl_state)
      {:ok, updated_dsl_state}
  """
  @impl true
  def transform(dsl_state) do
    resource_module = Transformer.get_persisted(dsl_state, :module)
    
    with projections when is_list(projections) and projections != [] <- 
           Transformer.get_entities(dsl_state, [:commanded, :projections]) do
      
      resource_name = BaseTransformer.get_resource_name(resource_module)
      app_prefix = BaseTransformer.get_module_prefix(resource_module)
      
      final_state = Enum.reduce(projections, dsl_state, fn projection, acc_dsl_state ->
        projection_module = build_projection_module(projection, app_prefix)
        BaseTransformer.create_module(projection_module, build_projection_module_ast(projection, resource_name), __ENV__)
        
        # Store the generated module in DSL state for potential use by other transformers
        projection_modules = Transformer.get_persisted(acc_dsl_state, :projection_modules, [])
        updated_dsl_state = Transformer.persist(acc_dsl_state, :projection_modules, [
          {projection.name, projection_module} | projection_modules
        ])
        
        updated_dsl_state
      end)
      
      {:ok, final_state}
    else
      _ -> {:ok, dsl_state}
    end
  end
  
  @doc """
  Builds the module name for a projection.
  
  ## Examples
  
      iex> build_projection_module(%Projection{name: :user_registered}, MyApp)
      MyApp.Projections.UserRegistered
  """
  def build_projection_module(projection, app_prefix) do
    projection_name_str = BaseTransformer.camelize_atom(projection.name)
    
    Module.concat([app_prefix, "Projections", projection_name_str])
  end
  
  @doc """
  Builds the AST (Abstract Syntax Tree) for a projection module.
  
  ## Examples
  
      iex> build_projection_module_ast(%Projection{name: :user_registered, action: :create, changes: %{status: :active}}, "User")
      {:__block__, [], [{:@, [...], [{:moduledoc, [...], [...]}]}, ...]}
  """
  def build_projection_module_ast(projection, resource_name) do
    event_name = projection.event_name || projection.name
    
    # Create a more descriptive moduledoc
    moduledoc = "Projection that handles #{event_name} events for #{resource_name} resources"
    
    # Generate the appropriate apply function based on the changes type
    changes_ast = generate_changes_ast(projection.changes)
    
    quote do
      @moduledoc unquote(moduledoc)
      
      @doc """
      Applies the projection to the resource based on the received event.
      
      ## Parameters
      
      - `event` - The event that triggered this projection
      - `resource` - The resource to apply changes to
      
      ## Returns
      
      An Ash changeset ready to be applied
      """
      def apply(event, resource) do
        changes = unquote(changes_ast)
        
        Ash.Changeset.new(resource)
        |> Ash.Changeset.for_action(unquote(projection.action), changes)
        |> Ash.Changeset.set_context(%{event: event})
      end
      
      @doc """
      Returns the action that should be performed on the resource.
      
      ## Returns
      
      The atom representing the Ash action to perform
      """
      def action, do: unquote(projection.action)
    end
  end
  
  # Generate AST for static map changes
  defp generate_changes_ast(changes) when is_map(changes) do
    # Handle maps with function values
    changes_with_handled_functions =
      Enum.map(changes, fn {k, v} ->
        {k, handle_function_value(v)}
      end)
      |> Enum.into(%{})
      
    Macro.escape(changes_with_handled_functions)
  end
  
  # Generate AST for function reference in DSL
  # Note: Spark will store this as a quoted function expression, not an actual function
  defp generate_changes_ast({:fn, _, _} = fn_ast) do
    quote do
      changes_fn = unquote(fn_ast)
      changes_fn.(event)
    end
  end
  
  # Handle any other function-like construct (e.g., &Module.func/0)
  defp generate_changes_ast(other) when is_function(other) do
    quote do
      unquote(Macro.escape(other))
    end
  end
  
  # Handle function reference passed in a map value
  defp handle_function_value(value) when is_function(value) do
    # For function values, we'll just reference them directly
    # This will be escaped by Macro.escape in the parent function
    value
  end
  
  # Handle non-function values
  defp handle_function_value(value) do
    value
  end
end