defmodule AshCommanded.Commanded.Transformers.GenerateProjectorModules do
  @moduledoc """
  Generates projector modules based on the projections defined in the DSL.
  
  For each resource with projections, this transformer will generate a projector module
  that subscribes to the events and applies the corresponding projections to update
  the resource state.
  
  This transformer should run after the projection module transformer.
  
  ## Example
  
  Given a resource with several projections, this transformer will generate:
  
  ```elixir
  defmodule MyApp.Projectors.UserProjector do
    @moduledoc "Event handler that projects events onto the User resource"
    
    use Commanded.Event.Handler,
      application: MyApp.CommandedApplication,
      name: "MyApp.Projectors.UserProjector"
      
    alias MyApp.Projections
    
    def handle(%MyApp.Events.UserRegistered{} = event, _metadata) do
      with {:ok, resource} <- Projections.UserRegistered.apply(event),
           {:ok, _result} <- perform_action(resource, Projections.UserRegistered.action()) do
        :ok
      else
        {:error, reason} -> {:error, reason}
      end
    end
    
    # More handlers for other events...
    
    defp perform_action(changeset, :create), do: Ash.create(changeset)
    defp perform_action(changeset, :update), do: Ash.update(changeset)
    defp perform_action(changeset, :destroy), do: Ash.destroy(changeset)
  end
  ```
  """
  
  use Spark.Dsl.Transformer
  alias Spark.Dsl.Transformer
  alias AshCommanded.Commanded.Transformers.BaseTransformer
  alias AshCommanded.Commanded.Transformers.GenerateProjectionModules
  
  @doc """
  Specifies that this transformer should run after the projection module transformer.
  """
  @impl true
  def after?(GenerateProjectionModules), do: true
  def after?(_), do: false
  
  @doc """
  Transforms the DSL state to generate projector 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)
    
    projections = Transformer.get_entities(dsl_state, [:commanded, :projections])
    events = Transformer.get_entities(dsl_state, [:commanded, :events])
    
    # Only proceed if there are projections that should be autogenerated
    case Enum.filter(projections, & &1.autogenerate?) do
      [] ->
        {:ok, dsl_state}
        
      autogen_projections ->
        # Get the previously generated modules from DSL state
        event_modules = Transformer.get_persisted(dsl_state, :event_modules, [])
        projection_modules = Transformer.get_persisted(dsl_state, :projection_modules, [])
        
        # Build connections between events and projections
        event_projections = build_event_projection_map(autogen_projections, events)
        
        # Create the projector module
        app_prefix = BaseTransformer.get_module_prefix(resource_module)
        resource_name = BaseTransformer.get_resource_name(resource_module)
        
        projector_module = build_projector_module(resource_name, app_prefix)
        
        # Create the module AST and define it
        ast = build_projector_module_ast(
          resource_module,
          resource_name, 
          autogen_projections,
          event_projections,
          event_modules,
          projection_modules
        )
        
        # Skip actual module creation in test environment
        unless Application.get_env(:ash_commanded, :skip_projector_module_creation, Mix.env() == :test) do
          BaseTransformer.create_module(projector_module, ast, __ENV__)
        end
        
        # Store the generated module in DSL state
        updated_dsl_state = Transformer.persist(dsl_state, :projector_modules, [
          {resource_module, projector_module}
        ])
        
        {:ok, updated_dsl_state}
    end
  end
  
  @doc """
  Builds a map of event names to their corresponding projections.
  
  ## Examples
  
      iex> build_event_projection_map(projections, events)
      %{user_registered: [projection1, projection2], email_changed: [projection3]}
  """
  def build_event_projection_map(projections, _events) do
    # We include _events parameter for future enhancements
    # Group projections by the event they respond to
    projections
    |> Enum.group_by(
      fn projection ->
        # Use the explicit event_name or fallback to the projection name
        projection.event_name || projection.name
      end,
      fn projection -> projection end
    )
  end
  
  @doc """
  Builds the module name for a projector.
  
  ## Examples
  
      iex> build_projector_module("User", MyApp)
      MyApp.Projectors.UserProjector
  """
  def build_projector_module(resource_name, app_prefix) do
    Module.concat([app_prefix, "Projectors", "#{resource_name}Projector"])
  end
  
  @doc """
  Builds the AST (Abstract Syntax Tree) for a projector module.
  
  ## Examples
  
      iex> build_projector_module_ast(MyApp.User, "User", projections, event_map, event_modules, projection_modules)
      {:__block__, [], [{:@, [...], [{:moduledoc, [...], [...]}]}, ...]}
  """
  def build_projector_module_ast(
    resource_module,
    resource_name, 
    _projections, 
    event_projections, 
    event_modules, 
    projection_modules
  ) do
    # Create a more descriptive moduledoc
    moduledoc = "Event handler that projects events onto the #{resource_name} resource"
    
    # Generate handle function for each event type
    event_handlers = 
      event_projections
      |> Enum.map(fn {event_name, event_projections} ->
        build_event_handler(event_name, event_projections, event_modules, projection_modules)
      end)
    
    # Basic application name - would need to be configurable in production
    application_name = :"#{resource_module}.CommandedApplication"
    projector_name = "#{resource_module}Projector"
    
    # Action-specific implementation functions for each Ash action
    action_handlers = build_action_handlers()
    
    # Check if Commanded module exists, else use stub code
    # This is necessary for testing environments where Commanded isn't available
    if Code.ensure_loaded?(Commanded) do
      quote do
        @moduledoc unquote(moduledoc)
        
        use Commanded.Event.Handler,
          application: unquote(application_name),
          name: unquote(projector_name)
      end
    else
      quote do
        @moduledoc unquote(moduledoc)
        
        # Define a stub implementation for testing
        def init(config), do: {:ok, config}
      end
    end
    |> then(fn quoted_core ->
      quote do
        unquote(quoted_core)
        
        # Common parts regardless of Commanded availability
        
        # Aliases for the projection modules
        alias Ash.Changeset
        alias Ash.Resource
        
        # Import resource module to get Ash actions
        import unquote(resource_module)
        
        unquote_splicing(event_handlers)
        
        unquote_splicing(action_handlers)
      end
    end)
  end
  
  # Build an event handler function for a specific event and its projections
  defp build_event_handler(event_name, projections, event_modules, projection_modules) do
    # Look up the event module for this event
    event_module = event_modules[event_name]
    
    projection_handlers =
      projections
      |> Enum.map(fn projection ->
        # Look up the projection module for this projection
        projection_module = projection_modules[projection.name]
        
        quote do
          with {:ok, changeset} <- unquote(projection_module).apply(event, unquote(projection.name)),
               {:ok, result} <- perform_action(changeset, unquote(projection_module).action()) do
            {:ok, result}
          else
            {:error, reason} -> {:error, reason}
          end
        end
      end)
    
    # Generate handler function for single projection case vs multiple
    case projection_handlers do
      [single_handler] ->
        # Handle a single projection directly
        quote do
          def handle(%unquote(event_module){} = event, _metadata) do
            unquote(single_handler)
          end
        end
        
      _multiple_handlers ->
        # Handle multiple projections with a multi_dispatch
        quote do
          def handle(%unquote(event_module){} = event, _metadata) do
            results = [
              unquote_splicing(projection_handlers)
            ]
            
            # If any projection fails, the whole handler fails
            case Enum.filter(results, fn 
              {:error, _} -> true
              _ -> false
            end) do
              [] -> :ok
              [error | _] -> error
            end
          end
        end
    end
  end
  
  # Build handler functions for different Ash actions
  defp build_action_handlers do
    [
      quote do
        defp perform_action(changeset, :create), do: Ash.create(changeset)
      end,
      quote do
        defp perform_action(changeset, :update), do: Ash.update(changeset)
      end,
      quote do
        defp perform_action(changeset, :destroy), do: Ash.destroy(changeset)
      end,
      quote do
        defp perform_action(changeset, :read), do: Ash.read(changeset)
      end,
      quote do
        defp perform_action(changeset, action) when is_atom(action) do
          # For custom actions, use Ash.run_action
          Ash.run_action(changeset, action)
        end
      end
    ]
  end
end