defmodule AshJido.Generator do
  @moduledoc """
  Generates Jido.Action modules from Ash actions at compile time.

  This module is responsible for:
  - Analyzing Ash action definitions
  - Converting Ash types to NimbleOptions schemas
  - Generating the complete Jido.Action module AST
  - Ensuring proper parameter and context handling
  """

  alias AshJido.TypeMapper
  alias Spark.Dsl.Transformer

  @doc """
  Generates a Jido.Action module for the given Ash action.

  Returns the module name that was generated.
  """
  def generate_jido_action_module(resource, jido_action, dsl_state) do
    ash_action = get_ash_action(resource, jido_action.action, dsl_state)
    module_name = build_module_name(resource, jido_action, ash_action)

    # Generate the module AST and compile it
    module_ast = build_module_ast(resource, ash_action, jido_action, module_name)

    # Check if module needs compilation
    should_compile =
      case Code.ensure_loaded(module_name) do
        {:module, _} ->
          # Module exists, check if it has the expected interface
          # In test env, recompile if the name doesn't match expectations
          if Mix.env() == :test do
            try do
              current_name = apply(module_name, :name, [])
              current_name != jido_action.name
            rescue
              # If name/0 doesn't exist or fails, recompile
              _ -> true
            end
          else
            false
          end

        {:error, _} ->
          # Module doesn't exist, compile it
          true
      end

    if should_compile do
      Code.compile_quoted(module_ast)
    end

    module_name
  end

  defp get_ash_action(resource, action_name, dsl_state) do
    # Get all actions from the actions section using Spark transformer
    all_actions = Transformer.get_entities(dsl_state, [:actions])

    Enum.find(all_actions, fn action ->
      action.name == action_name
    end) ||
      raise "Action #{action_name} not found in resource #{inspect(resource)}. Available: #{inspect(Enum.map(all_actions, &{&1.name, &1.type}))}"
  end

  defp build_module_name(resource, jido_action, ash_action) do
    case jido_action.module_name do
      nil ->
        # Use default module naming
        _resource_name = resource |> Module.split() |> List.last()
        action_name = ash_action.name |> to_string() |> Macro.camelize()

        base_module = Module.concat([resource, "Jido"])
        Module.concat([base_module, action_name])

      custom_module_name ->
        # Use the custom module name provided in DSL
        custom_module_name
    end
  end

  defp build_module_ast(resource, ash_action, jido_action, module_name) do
    action_name = jido_action.name || build_default_action_name(resource, ash_action)

    description =
      jido_action.description || ash_action.description || "Ash action: #{ash_action.name}"

    # Build enhanced configuration
    {schema, output_schema} = build_schemas(ash_action)
    category = build_category(ash_action)
    tags = build_tags(resource, ash_action, jido_action)

    quote do
      defmodule unquote(module_name) do
        @moduledoc """
        Generated Jido.Action for Ash action `#{unquote(ash_action.name)}` 
        from resource `#{unquote(resource)}`.

        This module was automatically generated by AshJido and provides a bridge
        between Ash resources and Jido actions, enabling seamless integration
        with agent-based systems and AI toolchains.

        ## Ash Integration

        - **Resource**: `#{unquote(resource)}`
        - **Action**: `#{unquote(ash_action.name)}` (#{unquote(ash_action.type)})
        - **Generated**: #{DateTime.utc_now() |> DateTime.to_iso8601()}

        ## Usage

        ```elixir
        # Execute the action with parameters and context
        #{unquote(module_name)}.run(params, context)
        ```

        ## Parameters

        #{unquote(generate_parameter_docs(schema))}

        ## Examples

        ```elixir
        # Basic usage
        params = #{unquote(generate_example_params(ash_action))}
        context = %{domain: YourDomain}

        case #{unquote(module_name)}.run(params, context) do
          {:ok, result} -> 
            # Success - result contains the #{unquote(ash_action.type)} operation output
            IO.inspect(result)
          {:error, error} -> 
            # Handle error
            IO.inspect(error)
        end
        ```

        ## AI Tool Integration

        This action can be converted to an AI tool format:

        ```elixir
        tool_definition = #{unquote(module_name)}.to_tool()
        # Use with LLM function calling systems
        ```
        """

        use Jido.Action,
          name: unquote(action_name),
          description: unquote(description),
          category: unquote(category),
          tags: unquote(tags),
          vsn: "1.0.0",
          schema: unquote(Macro.escape(schema)),
          output_schema: unquote(Macro.escape(output_schema)),
          compensation: [
            enabled: true,
            max_retries: 3,
            timeout: 30_000
          ]

        import Ash.Expr

        @resource unquote(resource)
        @ash_action unquote(ash_action.name)
        @ash_action_type unquote(ash_action.type)
        @jido_config unquote(Macro.escape(jido_action))

        def run(params, context) do
          # Extract actor and tenant from context
          actor = context[:actor]
          tenant = context[:tenant]
          domain = context[:domain] || determine_domain()

          # Execute the Ash action
          try do
            case unquote(ash_action.type) do
              :create ->
                result =
                  @resource
                  |> Ash.Changeset.for_create(@ash_action, params,
                    actor: actor,
                    tenant: tenant,
                    domain: domain
                  )
                  |> Ash.create!()

                {:ok, result} |> AshJido.Mapper.wrap_result(@jido_config)

              :read ->
                result =
                  @resource
                  |> Ash.Query.new()
                  |> maybe_apply_filters(params)
                  |> Ash.Query.set_context(%{actor: actor, tenant: tenant})
                  |> Ash.read!(domain: domain)

                {:ok, result} |> AshJido.Mapper.wrap_result(@jido_config)

              :update ->
                # Load the record to update using its primary key
                record_id = Map.get(params, :id) || Map.get(params, "id")

                unless record_id do
                  raise ArgumentError, "Update actions require an 'id' parameter"
                end

                # Remove id from params to prevent it being passed to changeset
                update_params = Map.drop(params, [:id, "id"])

                # Load the record first
                record =
                  @resource
                  |> Ash.get!(record_id, domain: domain, actor: actor, tenant: tenant)

                # Update the record
                result =
                  record
                  |> Ash.Changeset.for_update(@ash_action, update_params,
                    actor: actor,
                    tenant: tenant,
                    domain: domain
                  )
                  |> Ash.update!()

                {:ok, result} |> AshJido.Mapper.wrap_result(@jido_config)

              :destroy ->
                # Load the record to destroy using its primary key
                record_id = Map.get(params, :id) || Map.get(params, "id")

                unless record_id do
                  raise ArgumentError, "Destroy actions require an 'id' parameter"
                end

                # Load the record first
                record =
                  @resource
                  |> Ash.get!(record_id, domain: domain, actor: actor, tenant: tenant)

                # Destroy the record
                result =
                  record
                  |> Ash.Changeset.for_destroy(@ash_action, %{},
                    actor: actor,
                    tenant: tenant,
                    domain: domain
                  )
                  |> Ash.destroy!()

                {:ok, result} |> AshJido.Mapper.wrap_result(@jido_config)

              :action ->
                # Generic actions are custom actions that can return any type
                # They are called using Ash.run_action/2
                result =
                  @resource
                  |> Ash.ActionInput.for_action(@ash_action, params,
                    actor: actor,
                    tenant: tenant,
                    domain: domain
                  )
                  |> Ash.run_action!()

                result
                |> AshJido.Mapper.wrap_result(@jido_config)
                |> handle_action_result()
            end
          rescue
            error ->
              wrapped_error = AshJido.Mapper.wrap_result({:error, error}, @jido_config)
              on_error(params, error, context, [])
              wrapped_error
          end
        end

        # Enhanced lifecycle hooks for better integration

        @doc "Handles parameter preprocessing for Ash-specific needs"
        def on_before_validate_params(params) do
          # Convert string keys to atoms for Ash compatibility where needed
          normalized_params =
            case @ash_action_type do
              type when type in [:create, :update] ->
                # For data modification actions, ensure proper key format
                normalize_param_keys(params)

              _ ->
                params
            end

          {:ok, normalized_params}
        end

        @doc "Handles parameter postprocessing after validation"
        def on_after_validate_params(params) do
          # Add any Ash-specific parameter transformations
          {:ok, params}
        end

        @doc "Validates and processes action output"
        def on_after_run(result) do
          case result do
            {:ok, data} ->
              # Ensure output conforms to expected structure
              processed_data = process_ash_output(data)
              {:ok, processed_data}

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

        @doc "Handles errors with potential compensation"
        def on_error(failed_params, error, context, _opts) do
          # Log the error for debugging
          require Logger

          Logger.warning("Ash action #{@ash_action} failed",
            resource: @resource,
            params: failed_params,
            error: error,
            # Don't log sensitive actor data
            context: Map.drop(context, [:actor])
          )

          # For now, just return the failed params - compensation logic could be added here
          {:ok, failed_params}
        end

        # Helper function to handle final result processing
        defp handle_action_result({:ok, data} = result) do
          case on_after_run(result) do
            {:ok, processed_result} -> processed_result
            {:error, _} = error -> error
          end
        end

        defp handle_action_result({:error, _} = error), do: error

        # Helper function to normalize parameter keys for Ash compatibility
        defp normalize_param_keys(params) when is_map(params) do
          params
          |> Enum.map(fn
            {key, value} when is_binary(key) ->
              # Convert string keys to atoms for action arguments
              {String.to_existing_atom(key), value}

            {key, value} ->
              {key, value}
          end)
          |> Map.new()
        rescue
          ArgumentError ->
            # If atom doesn't exist, keep as string key
            params
        end

        # Helper function to process Ash output into proper format
        defp process_ash_output(data) do
          case @ash_action_type do
            :read when is_list(data) ->
              # For read actions returning lists, wrap in results structure
              %{results: data, count: length(data)}

            :read ->
              # For single record reads
              %{result: data}

            type when type in [:create, :update] ->
              # For data modification actions
              %{result: data, action: @ash_action}

            :destroy ->
              # For destroy actions
              %{deleted: true, action: @ash_action}

            :action ->
              # For custom actions, return as-is but ensure it's wrapped
              case data do
                %{} -> data
                _ -> %{result: data}
              end
          end
        end

        # Helper function to apply filters from params to query
        defp maybe_apply_filters(query, params) do
          # Apply basic filters for common parameters
          Enum.reduce(params, query, fn {key, value}, acc_query ->
            case {key, value} do
              {:id, id} when not is_nil(id) ->
                Ash.Query.filter_input(acc_query, %{id: id})

              {"id", id} when not is_nil(id) ->
                Ash.Query.filter_input(acc_query, %{id: id})

              {:limit, limit} when is_integer(limit) and limit > 0 ->
                Ash.Query.limit(acc_query, limit)

              {"limit", limit} when is_integer(limit) and limit > 0 ->
                Ash.Query.limit(acc_query, limit)

              {:offset, offset} when is_integer(offset) and offset >= 0 ->
                Ash.Query.offset(acc_query, offset)

              {"offset", offset} when is_integer(offset) and offset >= 0 ->
                Ash.Query.offset(acc_query, offset)

              # For other parameters, ignore them for now
              # In a more sophisticated implementation, these could be mapped to specific filters
              _ ->
                acc_query
            end
          end)
        end

        # Helper function to determine domain when not provided in context
        defp determine_domain do
          # Try to auto-discover domains that include this resource
          case discover_domains_for_resource(unquote(resource)) do
            [domain] ->
              # Single domain found - use it
              domain

            [domain | _] = domains ->
              # Multiple domains found - use the first one but warn
              IO.warn("""
              Multiple domains found for resource #{inspect(unquote(resource))}: #{inspect(domains)}
              Using the first one: #{inspect(domain)}
              To be explicit, provide a domain in context: %{domain: YourDomain}
              """)

              domain

            [] ->
              # No domains found - provide helpful error
              raise ArgumentError, """
              No domain found for resource #{inspect(unquote(resource))}.

              Please provide a domain in context:
              context = %{domain: YourDomain}
              #{unquote(module_name)}.run(params, context)

              Or ensure the resource is registered in a domain:
              defmodule YourDomain do
                use Ash.Domain
                resources do
                  resource #{inspect(unquote(resource))}
                end
              end
              """
          end
        end

        # Discover domains that contain this resource using Ash.Info
        defp discover_domains_for_resource(resource) do
          try do
            # Get all domains and their resources from the current OTP app
            case Application.get_application(unquote(resource)) do
              nil ->
                []

              otp_app ->
                otp_app
                |> Ash.Info.domains_and_resources()
                |> Enum.filter(fn {_domain, resources} ->
                  resource in resources
                end)
                |> Enum.map(fn {domain, _resources} -> domain end)
            end
          rescue
            # If Ash.Info is not available or fails, return empty list
            _ -> []
          end
        end
      end
    end
  end

  defp build_default_action_name(resource, ash_action) do
    resource_name =
      resource
      |> Module.split()
      |> List.last()
      |> Macro.underscore()

    # Create more intuitive action names based on type and context
    case ash_action.type do
      :create ->
        "create_#{resource_name}"

      :read ->
        # Use more descriptive names for read actions
        case ash_action.name do
          :get -> "get_#{resource_name}"
          :read -> "list_#{pluralize(resource_name)}"
          :by_id -> "get_#{resource_name}_by_id"
          name -> "#{resource_name}_#{name}"
        end

      :update ->
        "update_#{resource_name}"

      :destroy ->
        "delete_#{resource_name}"

      :action ->
        # For custom actions, use the action name as primary identifier
        case ash_action.name do
          name when name in [:activate, :deactivate, :archive, :restore] ->
            "#{name}_#{resource_name}"

          name ->
            "#{resource_name}_#{name}"
        end
    end
  end

  # Build both input and output schemas
  defp build_schemas(ash_action) do
    input_schema = build_parameter_schema(ash_action)
    output_schema = build_output_schema(ash_action)
    {input_schema, output_schema}
  end

  # Build category based on action type
  defp build_category(ash_action) do
    case ash_action.type do
      :create -> "data_creation"
      :read -> "data_retrieval"
      :update -> "data_modification"
      :destroy -> "data_deletion"
      :action -> "custom_operation"
    end
  end

  # Build tags from resource and action information
  defp build_tags(resource, ash_action, jido_action) do
    resource_name = resource |> Module.split() |> List.last() |> String.downcase()
    action_type = ash_action.type |> to_string()

    base_tags = ["ash", "resource", resource_name, action_type]

    # Add custom tags from jido_action if available
    custom_tags =
      case jido_action do
        %{tags: tags} when is_list(tags) -> tags
        _ -> []
      end

    (base_tags ++ custom_tags) |> Enum.uniq()
  end

  # Build output schema based on action type
  defp build_output_schema(ash_action) do
    case ash_action.type do
      :read ->
        [
          results: [type: {:list, :map}, doc: "List of records returned by the query"],
          count: [type: :non_neg_integer, doc: "Number of records returned"],
          result: [type: :map, doc: "Single record for get operations"]
        ]

      type when type in [:create, :update] ->
        [
          result: [type: :map, required: true, doc: "The created or updated record"],
          action: [type: :atom, required: true, doc: "The action that was executed"]
        ]

      :destroy ->
        [
          deleted: [
            type: :boolean,
            required: true,
            doc: "Whether the record was successfully deleted"
          ],
          action: [type: :atom, required: true, doc: "The action that was executed"]
        ]

      :action ->
        # For custom actions, provide flexible schema
        [
          result: [type: :any, doc: "The result of the custom action"]
        ]
    end
  end

  defp build_parameter_schema(ash_action) do
    case ash_action.type do
      :create ->
        # For create actions, use the action's arguments with enhanced documentation
        action_args_to_schema(ash_action.arguments || [])

      :read ->
        # For read actions, include common query parameters
        read_action_schema(ash_action)

      :update ->
        # For update actions, combine arguments with an id field
        update_action_schema(ash_action)

      :destroy ->
        # For destroy actions, typically just need an id
        destroy_action_schema(ash_action)

      :action ->
        # For generic actions, use the action's arguments
        action_args_to_schema(ash_action.arguments || [])
    end
  end

  defp action_args_to_schema(arguments) do
    Enum.map(arguments, fn arg ->
      {arg.name, TypeMapper.ash_type_to_nimble_options(arg.type, arg)}
    end)
  end

  defp read_action_schema(ash_action) do
    # Combine action arguments with common read parameters
    action_args = action_args_to_schema(ash_action.arguments || [])

    basic_params = [
      # Basic query parameters for read actions
      id: [type: :string, doc: "ID to filter by"],
      limit: [type: :integer, doc: "Maximum number of records to return"],
      offset: [type: :integer, doc: "Number of records to skip"]
    ]

    # Merge action-specific arguments with basic params, giving priority to action args
    action_args ++ basic_params
  end

  defp update_action_schema(ash_action) do
    base = [id: [type: :string, required: true, doc: "ID of record to update"]]
    args = action_args_to_schema(ash_action.arguments || [])
    base ++ args
  end

  defp destroy_action_schema(_ash_action) do
    [
      id: [type: :string, required: true, doc: "ID of record to destroy"]
    ]
  end

  # Generate parameter documentation for the module
  defp generate_parameter_docs(schema) do
    if Enum.empty?(schema) do
      "No parameters required."
    else
      schema
      |> Enum.map(fn {name, opts} ->
        required = if opts[:required], do: "(required)", else: "(optional)"
        doc = opts[:doc] || "Parameter #{name}"
        "- **#{name}** #{required}: #{doc}"
      end)
      |> Enum.join("\n        ")
    end
  end

  # Generate example parameters based on action type and schema
  defp generate_example_params(ash_action) do
    case ash_action.type do
      :create ->
        "%{name: \"John Doe\", email: \"john@example.com\"}"

      :read ->
        "%{limit: 10}"

      :update ->
        "%{id: \"123\", name: \"Updated Name\"}"

      :destroy ->
        "%{id: \"123\"}"

      :action ->
        "%{}"
    end
  end

  # Simple pluralization helper
  defp pluralize(word) do
    cond do
      String.ends_with?(word, "y") ->
        String.slice(word, 0..-2//-1) <> "ies"

      String.ends_with?(word, ["s", "sh", "ch", "x", "z"]) ->
        word <> "es"

      true ->
        word <> "s"
    end
  end
end
