if Code.ensure_loaded?(Igniter) do
  defmodule Mix.Tasks.Ash.Gen.Resource do
    @example """
    mix ash.gen.resource Helpdesk.Support.Ticket \\
      --default-actions read \\
      --uuid-primary-key id \\
      --attribute subject:string:required:public \\
      --relationship belongs_to:representative:Helpdesk.Support.Representative \\
      --timestamps \\
      --extend postgres,graphql
    """
    @moduledoc """
    Generate and configure an Ash.Resource.

    If the domain does not exist, we create it. If it does, we add the resource to it if it is not already present.

    ## Example

    ```bash
    #{@example}
    ```

    ## Options

    * `--attribute` or `-a` - An attribute or comma separated list of attributes to add, as `name:type`. Modifiers: `primary_key`, `array`, `public`, `sensitive`, and `required`. i.e `-a name:string:required`
    * `--relationship` or `-r` - A relationship or comma separated list of relationships to add, as `type:name:dest`. Modifiers: `public` and `sensitive?`. `belongs_to` only modifiers: `primary_key` and `required`. i.e `-r belongs_to:author:MyApp.Accounts.Author:required`. For many_to_many relationship the through relationship is required between name and destination, i.e. `-r many_to_many:posts:MyApp.Blog.PostComment:MyApp.Blog.Comment:public`
    * `--default-actions` - A csv list of default action types to add. The `create` and `update` actions accept the public attributes being added.
    * `--uuid-primary-key` or `-u` - Adds a UUIDv4 primary key with that name. i.e `-u id`
    * `--uuid-v7-primary-key` - Adds a UUIDv7 primary key with that name.
    * `--integer-primary-key` or `-i` - Adds an integer primary key with that name. i.e `-i id`
    * `--domain` or `-d` - The domain module to add the resource to. i.e `-d MyApp.MyDomain`. This defaults to the resource's module name, minus the last segment.
    * `--extend` or `-e` - A comma separated list of modules or builtins to extend the resource with. i.e `-e postgres,Some.Extension`
    * `--base` or `-b` - The base module to use for the resource. i.e `-b Ash.Resource`. Requires that the module is in `config :your_app, :base_resources`
    * `--timestamps` or `-t` - If set adds `inserted_at` and `updated_at` timestamps to the resource.
    * `--ignore-if-exists` - Does nothing if the resource already exists
    * `--conflicts` - How to handle conflicts when the same attribute, relationship, or action already exists. Options: `ignore` (default), `replace`
       `ignore` will ignore your addition for that attribute, relationship, or action. `replace` will remove the existing one in favor of yours.
    """

    @shortdoc "Generate and configure an Ash.Resource."
    use Igniter.Mix.Task

    @impl Igniter.Mix.Task
    def info(argv, _parent) do
      for {key, cmd} <- [da: "--default-actions", u7: "--uuid-v7-primary-key"] do
        if "-#{key}" in argv do
          Mix.shell().error("""
            The `-#{key}` alias has been removed as multi-char aliases are deprecated in OptionParser.
            Please use `--#{cmd}` instead.
          """)

          Mix.shell().exit({:shutdown, 1})
        end
      end

      %Igniter.Mix.Task.Info{
        positional: [:resource],
        example: @example,
        schema: [
          attribute: :csv,
          relationship: :csv,
          default_actions: :csv,
          uuid_primary_key: :string,
          uuid_v7_primary_key: :string,
          integer_primary_key: :string,
          domain: :string,
          extend: :csv,
          base: :string,
          timestamps: :boolean,
          da: :string,
          u7: :string,
          ignore_if_exists: :boolean,
          conflicts: :string
        ],
        aliases: [
          a: :attribute,
          r: :relationship,
          d: :domain,
          u: :uuid_primary_key,
          i: :integer_primary_key,
          e: :extend,
          b: :base,
          t: :timestamps
        ]
      }
    end

    @impl Igniter.Mix.Task
    def igniter(igniter) do
      arguments = igniter.args.positional
      options = igniter.args.options
      argv = igniter.args.argv_flags

      resource = Igniter.Project.Module.parse(arguments.resource)
      app_name = Igniter.Project.Application.app_name(igniter)

      {exists?, igniter} = Igniter.Project.Module.module_exists(igniter, resource)

      if "--ignore-if-exists" in igniter.args.argv_flags && exists? do
        igniter
      else
        domain =
          case options[:domain] do
            nil ->
              resource
              |> Module.split()
              |> :lists.droplast()
              |> Module.concat()

            domain ->
              Igniter.Project.Module.parse(domain)
          end

        # Validate conflicts option
        conflicts_strategy =
          case options[:conflicts] do
            nil ->
              "ignore"

            strategy when strategy in ["ignore", "replace"] ->
              strategy

            invalid ->
              raise """
              Invalid value for --conflicts: #{inspect(invalid)}

              Valid options are: ignore, replace
              """
          end

        options =
          options
          |> Keyword.update(
            :default_actions,
            [],
            fn defaults -> Enum.sort_by(defaults, &(&1 in ["create", "update"])) end
          )
          |> Keyword.put_new(:base, "Ash.Resource")
          |> Keyword.put(:conflicts, conflicts_strategy)

        base =
          if options[:base] == "Ash.Resource" do
            "Ash.Resource"
          else
            base =
              Igniter.Project.Module.parse(options[:base])

            if base not in List.wrap(Application.get_env(app_name, :base_resources)) do
              raise """
              The base module #{inspect(base)} is not in the list of base resources.

              If it exists but is not in the base resource list, add it like so:

              `config #{inspect(app_name)}, base_resources: [#{inspect(base)}]`

              If it does not exist, you can generate a base resource with `mix ash.gen.base_resource #{inspect(base)}`
              """
            end

            inspect(base)
          end

        default_accept =
          Enum.flat_map(options[:attribute] || [], fn attribute ->
            case String.split(attribute, ":", trim: true) do
              [name, _type | modifiers] ->
                if "public" in modifiers do
                  [String.to_atom(name)]
                else
                  []
                end

              _ ->
                raise """
                Invalid attribute format: #{attribute}. Please use the format `name:type` for each attribute.
                """
            end
          end)

        igniter
        |> Igniter.compose_task("ash.gen.domain", [inspect(domain), "--ignore-if-exists"])
        |> Ash.Domain.Igniter.add_resource_reference(
          domain,
          resource
        )
        |> ensure_resource_exists(resource, base, app_name, domain)
        |> add_attributes_to_resource(resource, options)
        |> add_relationships_to_resource(resource, options)
        |> add_actions_to_resource(resource, options, default_accept)
        |> extend(resource, options[:extend], argv)
      end
    end

    defp extend(igniter, _, [], _) do
      igniter
    end

    defp extend(igniter, resource, extensions, argv) do
      Igniter.compose_task(
        igniter,
        "ash.extend",
        [inspect(resource), Enum.join(extensions, ",")] ++ argv
      )
    end

    defp valid_attribute_name?(name) do
      Regex.match?(~r/^[a-zA-Z][a-zA-Z0-9_]*[!?]?$/, name)
    end

    defp attribute_modifier_string(modifiers) do
      modifiers
      |> Enum.uniq()
      |> Enum.map_join("\n", fn
        "primary_key" ->
          "primary_key? true"

        "public" ->
          "public? true"

        "required" ->
          "allow_nil? false"

        "sensitive" ->
          "sensitive? true"

        "array" ->
          "array"

        unknown ->
          raise ArgumentError,
                """
                Unrecognizeable attribute modifier: `#{unknown}`.

                Known modifiers are: primary_key, public, required, sensitive.
                """
      end)
    end

    defp resolve_type(value) do
      # sadly, we can't validate the type here
      # because the type may be being generated
      # as part of the task that generates this resource
      if String.contains?(value, ".") do
        Module.concat([value])
      else
        String.to_atom(value)
      end
    end

    defp ensure_resource_exists(igniter, resource, base, app_name, domain) do
      case Igniter.Project.Module.find_module(igniter, resource) do
        {:ok, {igniter, _source, _zipper}} ->
          # Resource already exists, don't recreate it
          igniter

        {:error, igniter} ->
          # Resource doesn't exist, create it with basic structure
          Igniter.Project.Module.create_module(
            igniter,
            resource,
            """
            use #{base},
              otp_app: #{inspect(app_name)},
              domain: #{inspect(domain)}
            """
          )
      end
    end

    defp add_attributes_to_resource(igniter, resource, options) do
      igniter
      |> add_primary_key_to_resource(resource, options)
      |> add_regular_attributes_to_resource(resource, options)
      |> add_timestamps_to_resource(resource, options)
    end

    defp add_primary_key_to_resource(igniter, resource, options) do
      cond do
        options[:uuid_primary_key] ->
          add_primary_key_attribute(
            igniter,
            resource,
            "uuid_primary_key",
            options[:uuid_primary_key],
            options
          )

        options[:uuid_v7_primary_key] ->
          add_primary_key_attribute(
            igniter,
            resource,
            "uuid_v7_primary_key",
            options[:uuid_v7_primary_key],
            options
          )

        options[:integer_primary_key] ->
          add_primary_key_attribute(
            igniter,
            resource,
            "integer_primary_key",
            options[:integer_primary_key],
            options
          )

        true ->
          igniter
      end
    end

    defp add_primary_key_attribute(igniter, resource, builder, text, options) do
      [name | modifiers] = String.split(text, ":", trim: true)
      modifiers = modifiers -- ["primary_key"]
      name_atom = String.to_atom(name)

      if !valid_attribute_name?(name) do
        raise "Invalid attribute name provided for `#{builder}`: #{name}"
      end

      attribute_code =
        if Enum.empty?(modifiers) do
          "#{builder} :#{name}"
        else
          """
          #{builder} :#{name} do
            #{attribute_modifier_string(modifiers)}
          end
          """
        end

      add_attribute_with_conflicts(
        igniter,
        resource,
        name_atom,
        attribute_code,
        options[:conflicts]
      )
    end

    defp add_regular_attributes_to_resource(igniter, resource, options) do
      Enum.reduce(options[:attribute] || [], igniter, fn attribute, igniter ->
        case String.split(attribute, ":") do
          [name, type | modifiers] ->
            if !valid_attribute_name?(name) do
              raise "Invalid attribute name provided: #{name}"
            end

            name_atom = String.to_atom(name)

            {type, modifiers} =
              if Enum.any?(modifiers, &(&1 == "array")),
                do: {{:array, resolve_type(type)}, modifiers -- ["array"]},
                else: {resolve_type(type), modifiers}

            attribute_code =
              if Enum.empty?(modifiers) do
                "attribute :#{name}, #{inspect(type)}"
              else
                modifier_string = attribute_modifier_string(modifiers)

                """
                attribute :#{name}, #{inspect(type)} do
                  #{modifier_string}
                end
                """
              end

            add_attribute_with_conflicts(
              igniter,
              resource,
              name_atom,
              attribute_code,
              options[:conflicts]
            )

          _name ->
            raise """
            Invalid attribute format: #{attribute}. Please use the format `name:type` for each attribute.
            """
        end
      end)
    end

    defp add_timestamps_to_resource(igniter, resource, options) do
      if options[:timestamps] do
        case options[:conflicts] do
          "ignore" ->
            # Only add if no existing timestamps
            case find_existing_timestamps_call(igniter, resource) do
              # Already exists, ignore
              {:ok, igniter} ->
                igniter

              {:error, igniter} ->
                {igniter, has_timestamps?} =
                  Ash.Resource.Igniter.defines_attribute(igniter, resource, :inserted_at)

                if has_timestamps? do
                  igniter
                else
                  Ash.Resource.Igniter.add_attribute(igniter, resource, "timestamps()")
                end
            end

          "replace" ->
            # Remove existing and add new
            igniter
            |> remove_existing_timestamps(resource)
            |> Ash.Resource.Igniter.add_attribute(resource, "timestamps()")
        end
      else
        igniter
      end
    end

    defp add_relationships_to_resource(igniter, resource, options) do
      Enum.reduce(options[:relationship] || [], igniter, fn relationship, igniter ->
        case String.split(relationship, ":") do
          ["many_to_many", name, through, destination | modifiers] ->
            if !valid_attribute_name?(name) do
              raise "Invalid relationship name provided: #{name}"
            end

            name_atom = String.to_atom(name)

            relationship_code =
              if Enum.empty?(modifiers) do
                "many_to_many :#{name}, #{destination} do
                  through(#{through})
                end"
              else
                modifier_string =
                  Enum.map_join(modifiers, "\n", fn
                    "public" ->
                      "public? true"

                    "sensitive?" ->
                      "sensitive? true"

                    invalid_modifier ->
                      raise ArgumentError,
                            "Invalid modifier `#{invalid_modifier}` for many_to_many relationship, valid modifiers are `public` and `sensitive`"
                  end)

                """
                many_to_many :#{name}, #{destination} do
                  through(#{through})
                  #{modifier_string}
                end
                """
              end

            add_relationship_with_conflicts(
              igniter,
              resource,
              name_atom,
              relationship_code,
              options[:conflicts]
            )

          [type, name, destination | modifiers] ->
            if !valid_attribute_name?(name) do
              raise "Invalid relationship name provided: #{name}"
            end

            name_atom = String.to_atom(name)

            relationship_code =
              if Enum.empty?(modifiers) do
                "#{type} :#{name}, #{destination}"
              else
                modifier_string =
                  Enum.map_join(modifiers, "\n", fn
                    "primary_key" ->
                      if type == "belongs_to" do
                        "primary_key? true"
                      else
                        raise ArgumentError,
                              "The primary_key modifier is only valid for belongs_to relationships, saw it in `#{type}:#{name}`"
                      end

                    "public" ->
                      "public? true"

                    "sensitive?" ->
                      "sensitive? true"

                    "required" ->
                      if type == "belongs_to" do
                        "allow_nil? false"
                      else
                        raise ArgumentError,
                              "The ! modifier (for `allow_nil?: false`) is only valid for belongs_to relationships, saw it in `#{type}:#{name}`"
                      end
                  end)

                """
                #{type} :#{name}, #{destination} do
                  #{modifier_string}
                end
                """
              end

            add_relationship_with_conflicts(
              igniter,
              resource,
              name_atom,
              relationship_code,
              options[:conflicts]
            )

          _name ->
            raise """
            Invalid relationship format: #{relationship}. Please use the format `type:name:destination` for each attribute.
            """
        end
      end)
    end

    defp add_actions_to_resource(igniter, resource, options, default_accept) do
      case options[:default_actions] do
        [] ->
          igniter

        defaults ->
          case options[:conflicts] do
            "ignore" ->
              # Only add if no existing defaults call
              case find_existing_defaults_call(igniter, resource) do
                # Ignore if exists
                {:ok, igniter} ->
                  igniter

                {:error, igniter} ->
                  add_new_defaults_call(igniter, resource, defaults, default_accept)
              end

            "replace" ->
              # Remove existing and add new
              igniter
              |> remove_existing_defaults(resource)
              |> add_new_defaults_call(resource, defaults, default_accept)
          end
      end
    end

    defp find_existing_defaults_call(igniter, resource) do
      Spark.Igniter.find(igniter, resource, fn _, zipper ->
        with {:ok, zipper} <- enter_section(zipper, :actions),
             {:ok, _zipper} <-
               Igniter.Code.Function.move_to_function_call_in_current_scope(
                 zipper,
                 :defaults,
                 1
               ) do
          {:ok, true}
        else
          _ -> :error
        end
      end)
      |> case do
        {:ok, igniter, _module, _value} ->
          {:ok, igniter}

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

    defp find_existing_timestamps_call(igniter, resource) do
      Spark.Igniter.find(igniter, resource, fn _, zipper ->
        with {:ok, zipper} <- enter_section(zipper, :attributes),
             {:ok, _zipper} <-
               Igniter.Code.Function.move_to_function_call_in_current_scope(
                 zipper,
                 :timestamps,
                 [0, 1]
               ) do
          {:ok, true}
        else
          _ -> :error
        end
      end)
      |> case do
        {:ok, igniter, _module, _value} ->
          {:ok, igniter}

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

    defp add_new_defaults_call(igniter, resource, defaults, default_accept) do
      default_contents =
        Enum.map_join(defaults, ", ", fn
          type when type in ["read", "destroy"] ->
            ":#{type}"

          type when type in ["create", "update"] ->
            "#{type}: #{inspect(default_accept)}"

          type ->
            raise """
            Invalid default action type given to `--default-actions`: #{inspect(type)}.
            """
        end)

      actions_code = "defaults [#{default_contents}]"
      Ash.Resource.Igniter.add_block(igniter, resource, :actions, actions_code)
    end

    defp remove_existing_defaults(igniter, resource) do
      Igniter.Project.Module.find_and_update_module!(igniter, resource, fn zipper ->
        with {:ok, zipper} <- enter_section(zipper, :actions),
             {:ok, zipper} <-
               Igniter.Code.Function.move_to_function_call_in_current_scope(
                 zipper,
                 :defaults,
                 1
               ) do
          {:ok, Sourceror.Zipper.remove(zipper)}
        else
          _ -> {:ok, zipper}
        end
      end)
    end

    defp remove_existing_timestamps(igniter, resource) do
      Igniter.Project.Module.find_and_update_module!(igniter, resource, fn zipper ->
        with {:ok, zipper} <- enter_section(zipper, :attributes),
             {:ok, zipper} <-
               Igniter.Code.Function.move_to_function_call_in_current_scope(
                 zipper,
                 :timestamps,
                 [0, 1]
               ) do
          {:ok, Sourceror.Zipper.remove(zipper)}
        else
          _ -> {:ok, zipper}
        end
      end)
    end

    # Conflict-aware attribute handling
    defp add_attribute_with_conflicts(
           igniter,
           resource,
           name_atom,
           attribute_code,
           conflicts_strategy
         ) do
      case conflicts_strategy do
        "ignore" ->
          Ash.Resource.Igniter.add_new_attribute(igniter, resource, name_atom, attribute_code)

        "replace" ->
          igniter
          |> remove_existing_attribute(resource, name_atom)
          |> Ash.Resource.Igniter.add_attribute(resource, attribute_code)
      end
    end

    defp remove_existing_attribute(igniter, resource, name_atom) do
      Igniter.Project.Module.find_and_update_module!(igniter, resource, fn zipper ->
        with {:ok, zipper} <- enter_section(zipper, :attributes),
             {:ok, zipper} <- find_and_remove_attribute(zipper, name_atom) do
          {:ok, zipper}
        else
          _ -> {:ok, zipper}
        end
      end)
    end

    defp find_and_remove_attribute(zipper, name_atom) do
      case Igniter.Code.Function.move_to_function_call_in_current_scope(
             zipper,
             :attribute,
             [2, 3],
             &Igniter.Code.Function.argument_equals?(&1, 0, name_atom)
           ) do
        {:ok, zipper} ->
          {:ok, Sourceror.Zipper.remove(zipper)}

        :error ->
          # Try primary key functions
          primary_key_functions = [:uuid_primary_key, :uuid_v7_primary_key, :integer_primary_key]

          Enum.reduce_while(primary_key_functions, :error, fn function_name, _acc ->
            case Igniter.Code.Function.move_to_function_call_in_current_scope(
                   zipper,
                   function_name,
                   [1, 2],
                   &Igniter.Code.Function.argument_equals?(&1, 0, name_atom)
                 ) do
              {:ok, zipper} ->
                {:halt, {:ok, Sourceror.Zipper.remove(zipper)}}

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

    # Conflict-aware relationship handling
    defp add_relationship_with_conflicts(
           igniter,
           resource,
           name_atom,
           relationship_code,
           conflicts_strategy
         ) do
      case conflicts_strategy do
        "ignore" ->
          Ash.Resource.Igniter.add_new_relationship(
            igniter,
            resource,
            name_atom,
            relationship_code
          )

        "replace" ->
          igniter
          |> remove_existing_relationship(resource, name_atom)
          |> Ash.Resource.Igniter.add_relationship(resource, relationship_code)
      end
    end

    defp remove_existing_relationship(igniter, resource, name_atom) do
      Igniter.Project.Module.find_and_update_module!(igniter, resource, fn zipper ->
        with {:ok, zipper} <- enter_section(zipper, :relationships),
             {:ok, zipper} <- find_and_remove_relationship(zipper, name_atom) do
          {:ok, zipper}
        else
          _ -> {:ok, zipper}
        end
      end)
    end

    defp find_and_remove_relationship(zipper, name_atom) do
      relationship_functions = [:has_one, :has_many, :belongs_to, :many_to_many]

      Enum.reduce_while(relationship_functions, :error, fn function_name, _acc ->
        case Igniter.Code.Function.move_to_function_call_in_current_scope(
               zipper,
               function_name,
               [2, 3],
               &Igniter.Code.Function.argument_equals?(&1, 0, name_atom)
             ) do
          {:ok, zipper} ->
            {:halt, {:ok, Sourceror.Zipper.remove(zipper)}}

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

    defp enter_section(zipper, name) do
      with {:ok, zipper} <-
             Igniter.Code.Function.move_to_function_call_in_current_scope(
               zipper,
               name,
               1
             ) do
        Igniter.Code.Common.move_to_do_block(zipper)
      end
    end
  end
else
  defmodule Mix.Tasks.Ash.Gen.Resource do
    @moduledoc """
    Generate and configure an Ash.Resource.
    """

    @shortdoc "Generate and configure an Ash.Resource."

    use Mix.Task

    def run(_argv) do
      Mix.shell().error("""
      The task 'ash.gen.resource' requires igniter to be run.

      Please install igniter and try again.

      For more information, see: https://hexdocs.pm/igniter
      """)

      exit({:shutdown, 1})
    end
  end
end
