defmodule AshTypescript.Rpc.Codegen do
  @moduledoc """
  Generates TypeScript code for interacting with Ash resources via Rpc.
  """
  import AshTypescript.Codegen
  import AshTypescript.Filter
  import AshTypescript.Helpers

  alias AshTypescript.Rpc.RequestedFieldsProcessor
  alias AshTypescript.Rpc.ValidationErrorSchemas
  alias AshTypescript.Rpc.ZodSchemaGenerator

  def generate_typescript_types(otp_app, opts \\ []) do
    endpoint_process = Keyword.get(opts, :run_endpoint, "/rpc/run")
    endpoint_validate = Keyword.get(opts, :validate_endpoint, "/rpc/validate")

    resources_and_actions = get_rpc_resources_and_actions(otp_app)

    generate_full_typescript(
      resources_and_actions,
      endpoint_process,
      endpoint_validate,
      otp_app
    )
  end

  defp get_rpc_resources_and_actions(otp_app) do
    otp_app
    |> Ash.Info.domains()
    |> Enum.flat_map(fn domain ->
      rpc_config = AshTypescript.Rpc.Info.typescript_rpc(domain)

      Enum.flat_map(rpc_config, fn %{resource: resource, rpc_actions: rpc_actions} ->
        Enum.map(rpc_actions, fn rpc_action ->
          action = Ash.Resource.Info.action(resource, rpc_action.action)
          {resource, action, rpc_action}
        end)
      end)
    end)
  end

  defp get_typed_queries(otp_app) do
    otp_app
    |> Ash.Info.domains()
    |> Enum.flat_map(fn domain ->
      rpc_config = AshTypescript.Rpc.Info.typescript_rpc(domain)

      Enum.flat_map(rpc_config, fn %{resource: resource, typed_queries: typed_queries} ->
        Enum.map(typed_queries, fn typed_query ->
          action = Ash.Resource.Info.action(resource, typed_query.action)
          {resource, action, typed_query}
        end)
      end)
    end)
  end

  defp generate_imports() do
    zod_import =
      if AshTypescript.Rpc.generate_zod_schemas?() do
        zod_path = AshTypescript.Rpc.zod_import_path()
        "import { z } from \"#{zod_path}\";"
      else
        ""
      end

    config_imports =
      case Application.get_env(:ash_typescript, :import_into_generated) do
        nil ->
          ""

        imports when is_list(imports) ->
          imports
          |> Enum.map(fn import_config ->
            import_name = Map.get(import_config, :import_name)
            file_path = Map.get(import_config, :file)

            if import_name && file_path do
              "import * as #{import_name} from \"#{file_path}\";"
            else
              ""
            end
          end)
          |> Enum.reject(&(&1 == ""))
          |> Enum.join("\n")

        _ ->
          ""
      end

    all_imports =
      [zod_import, config_imports]
      |> Enum.reject(&(&1 == ""))
      |> Enum.join("\n")
      |> case do
        "" -> ""
        imports_str -> imports_str <> "\n"
      end

    all_imports
  end

  defp generate_full_typescript(
         rpc_resources_and_actions,
         endpoint_process,
         endpoint_validate,
         otp_app
       ) do
    rpc_resources =
      otp_app
      |> Ash.Info.domains()
      |> Enum.flat_map(fn domain ->
        AshTypescript.Rpc.Info.typescript_rpc(domain)
        |> Enum.map(fn %{resource: r} -> r end)
      end)

    actions =
      otp_app
      |> Ash.Info.domains()
      |> Enum.flat_map(fn domain ->
        AshTypescript.Rpc.Info.typescript_rpc(domain)
        |> Enum.flat_map(fn %{resource: resource, rpc_actions: rpc_actions} ->
          Enum.map(rpc_actions, fn %{action: action} ->
            Ash.Resource.Info.action(resource, action)
          end)
        end)
      end)

    typed_queries = get_typed_queries(otp_app)

    embedded_resources = find_embedded_resources(rpc_resources)
    typed_struct_modules = find_typed_struct_modules(rpc_resources)
    all_resources_for_schemas = rpc_resources ++ embedded_resources

    """
    // Generated by AshTypescript
    // Do not edit this file manually

    #{generate_imports()}

    #{generate_ash_type_aliases(rpc_resources, actions)}

    #{generate_all_schemas_for_resources(all_resources_for_schemas, all_resources_for_schemas)}

    #{ZodSchemaGenerator.generate_zod_schemas_for_embedded_resources(embedded_resources)}

    #{ValidationErrorSchemas.generate_validation_error_schemas_for_embedded_resources(embedded_resources)}

    #{ValidationErrorSchemas.generate_validation_error_schemas_for_typed_structs(typed_struct_modules)}

    #{generate_filter_types(all_resources_for_schemas, all_resources_for_schemas)}

    #{generate_utility_types()}

    #{generate_helper_functions()}

    #{generate_typed_queries_section(typed_queries, all_resources_for_schemas)}

    #{generate_rpc_functions(rpc_resources_and_actions, endpoint_process, endpoint_validate, otp_app, all_resources_for_schemas)}
    """
  end

  defp generate_utility_types do
    """
    // Utility Types

    // Resource schema constraint
    type TypedSchema = {
      __type: "Resource" | "TypedStruct" | "TypedMap" | "Union";
      __primitiveFields: string;
    };

    // Utility type to convert union to intersection
    type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
      k: infer I,
    ) => void
      ? I
      : never;

    type HasComplexFields<T extends TypedSchema> = keyof Omit<
      T,
      "__primitiveFields" | "__type" | T["__primitiveFields"]
    > extends never
      ? false
      : true;

    type ComplexFieldKeys<T extends TypedSchema> = keyof Omit<
      T,
      "__primitiveFields" | "__type" | T["__primitiveFields"]
    >;

    type LeafFieldSelection<T extends TypedSchema> = T["__primitiveFields"];

    type ComplexFieldSelection<T extends TypedSchema> = {
      [K in ComplexFieldKeys<T>]?: T[K] extends {
        __type: "Relationship";
        __resource: infer Resource;
      }
        ? NonNullable<Resource> extends TypedSchema
          ? UnifiedFieldSelection<NonNullable<Resource>>[]
          : never
        : T[K] extends {
              __type: "ComplexCalculation";
              __returnType: infer ReturnType;
            }
          ? T[K] extends { __args: infer Args }
            ? NonNullable<ReturnType> extends TypedSchema
              ? {
                  #{formatted_args_field()}: Args;
                  #{formatted_fields_field()}: UnifiedFieldSelection<NonNullable<ReturnType>>[];
                }
              : { #{formatted_args_field()}: Args }
            : NonNullable<ReturnType> extends TypedSchema
              ? { #{formatted_fields_field()}: UnifiedFieldSelection<NonNullable<ReturnType>>[] }
              : never
          : NonNullable<T[K]> extends TypedSchema
            ? UnifiedFieldSelection<NonNullable<T[K]>>[]
            : never;
    };

    // Main type: Use explicit base case detection to prevent infinite recursion
    type UnifiedFieldSelection<T extends TypedSchema> =
      HasComplexFields<T> extends false
        ? LeafFieldSelection<T> // Base case: only primitives, no recursion
        : LeafFieldSelection<T> | ComplexFieldSelection<T>; // Recursive case

    type InferFieldValue<
      T extends TypedSchema,
      Field,
    > = Field extends T["__primitiveFields"]
      ? Field extends keyof T
        ? { [K in Field]: T[Field] }
        : never
      : Field extends Record<string, any>
        ? {
            [K in keyof Field]: K extends keyof T
              ? T[K] extends {
                  __type: "Relationship";
                  __resource: infer Resource;
                }
                ? NonNullable<Resource> extends TypedSchema
                  ? T[K] extends { __array: true }
                    ? Array<InferResult<NonNullable<Resource>, Field[K]>>
                    : null extends Resource
                      ? InferResult<NonNullable<Resource>, Field[K]> | null
                      : InferResult<NonNullable<Resource>, Field[K]>
                : never
              : T[K] extends {
                    __type: "ComplexCalculation";
                    __returnType: infer ReturnType;
                  }
                ? NonNullable<ReturnType> extends TypedSchema
                  ? null extends ReturnType
                    ? InferResult<NonNullable<ReturnType>, Field[K]["fields"]> | null
                    : InferResult<NonNullable<ReturnType>, Field[K]["fields"]>
                  : ReturnType
                : NonNullable<T[K]> extends TypedSchema
                  ? null extends T[K]
                    ? InferResult<NonNullable<T[K]>, Field[K]> | null
                    : InferResult<NonNullable<T[K]>, Field[K]>
                  : never
              : never;
          }
        : never;

    type InferResult<
      T extends TypedSchema,
      SelectedFields extends UnifiedFieldSelection<T>[],
    > = UnionToIntersection<
      {
        [K in keyof SelectedFields]: InferFieldValue<T, SelectedFields[K]>;
      }[number]
    >;

    export type SuccessDataFunc<T extends (...args: any[]) => Promise<any>> = Extract<
      Awaited<ReturnType<T>>,
      { success: true }
    >["data"];


    export type ErrorData<T extends (...args: any[]) => Promise<any>> = Extract<
      Awaited<ReturnType<T>>,
      { success: false }
    >["errors"];

    /**
     * Represents an error from an unsuccessful RPC call
     * @example
     * const error: AshRpcError = { type: "validation_error", message: "Something went wrong" }
     */
    export type AshRpcError = {
      type: string;
      message: string;
      field?: string;
      fieldPath?: string;
      details?: Record<string, any>;
    }



    """
  end

  defp generate_helper_functions do
    """
    // Helper Functions

    /**
     * Gets the CSRF token from the page's meta tag
     * Returns null if no CSRF token is found
     */
    export function getPhoenixCSRFToken(): string | null {
      return document
        ?.querySelector("meta[name='csrf-token']")
        ?.getAttribute("content") || null;
    }

    /**
     * Builds headers object with CSRF token for Phoenix applications
     * Returns headers object with X-CSRF-Token (if available)
     */
    export function buildCSRFHeaders(headers: Record<string, string> = {}): Record<string, string> {
      const csrfToken = getPhoenixCSRFToken();
      if (csrfToken) {
        headers["X-CSRF-Token"] = csrfToken;
      }

      return headers;
    }

    """
  end

  defp generate_rpc_functions(
         resources_and_actions,
         endpoint_process,
         endpoint_validate,
         otp_app,
         _resources
       ) do
    rpc_functions =
      resources_and_actions
      |> Enum.map(fn resource_and_action ->
        generate_rpc_function(
          resource_and_action,
          resources_and_actions,
          endpoint_process,
          endpoint_validate,
          otp_app
        )
      end)
      |> Enum.join("\n\n")

    """
    #{rpc_functions}
    """
  end

  def action_supports_pagination?(action) do
    action.type == :read and not action.get? and has_pagination_config?(action)
  end

  defp action_supports_offset_pagination?(action) do
    case get_pagination_config(action) do
      nil -> false
      pagination_config -> Map.get(pagination_config, :offset?, false)
    end
  end

  defp action_supports_keyset_pagination?(action) do
    case get_pagination_config(action) do
      nil -> false
      pagination_config -> Map.get(pagination_config, :keyset?, false)
    end
  end

  defp action_requires_pagination?(action) do
    case get_pagination_config(action) do
      nil -> false
      pagination_config -> Map.get(pagination_config, :required?, false)
    end
  end

  defp action_supports_countable?(action) do
    case get_pagination_config(action) do
      nil -> false
      pagination_config -> Map.get(pagination_config, :countable, false)
    end
  end

  defp action_has_default_limit?(action) do
    case get_pagination_config(action) do
      nil -> false
      pagination_config -> Map.has_key?(pagination_config, :default_limit)
    end
  end

  defp has_pagination_config?(action) do
    case action do
      %{pagination: pagination} when is_map(pagination) -> true
      _ -> false
    end
  end

  defp get_pagination_config(action) do
    case action do
      %{pagination: pagination} when is_map(pagination) -> pagination
      _ -> nil
    end
  end

  defp action_has_input?(resource, action) do
    case action.type do
      :read ->
        action.arguments != []

      :create ->
        accepts = Ash.Resource.Info.action(resource, action.name).accept || []
        accepts != [] || action.arguments != []

      action_type when action_type in [:update, :destroy] ->
        action.accept != [] || action.arguments != []

      :action ->
        action.arguments != []
    end
  end

  def action_returns_field_selectable_type?(action) do
    case action.returns do
      {:array, Ash.Type.Struct} ->
        items_constraints = Keyword.get(action.constraints || [], :items, [])

        if Keyword.has_key?(items_constraints, :instance_of) do
          {:ok, :array_of_resource, Keyword.get(items_constraints, :instance_of)}
        else
          {:error, :no_instance_of_defined}
        end

      Ash.Type.Struct ->
        constraints = action.constraints || []

        if Keyword.has_key?(constraints, :instance_of) do
          {:ok, :resource, Keyword.get(constraints, :instance_of)}
        else
          {:error, :no_instance_of_defined}
        end

      {:array, map_like} when map_like in [Ash.Type.Map, Ash.Type.Keyword, Ash.Type.Keyword] ->
        items_constraints = Keyword.get(action.constraints || [], :items, [])

        if Keyword.has_key?(items_constraints, :fields) do
          {:ok, :array_of_typed_map, Keyword.get(items_constraints, :fields)}
        else
          {:error, :no_fields_defined}
        end

      map_like when map_like in [Ash.Type.Map, Ash.Type.Keyword, Ash.Type.Keyword] ->
        constraints = action.constraints || []

        if Keyword.has_key?(constraints, :fields) do
          {:ok, :typed_map, Keyword.get(constraints, :fields)}
        else
          {:error, :no_fields_defined}
        end

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

  defp generate_typed_queries_section([], _all_resources), do: ""

  defp generate_typed_queries_section(typed_queries, all_resources) do
    queries_by_resource =
      Enum.group_by(typed_queries, fn {resource, _action, _query} -> resource end)

    sections =
      Enum.map(queries_by_resource, fn {resource, queries} ->
        resource_name = build_resource_type_name(resource)

        query_types_and_consts =
          Enum.map(queries, fn {resource, action, typed_query} ->
            generate_typed_query_type_and_const(resource, action, typed_query, all_resources)
          end)

        """
        // #{resource_name} Typed Queries
        #{Enum.join(query_types_and_consts, "\n\n")}
        """
      end)

    """
    // ============================
    // Typed Queries
    // ============================
    // Use these types and field constants for server-side rendering and data fetching.
    // The field constants can be used with the corresponding RPC actions for client-side refetching.

    #{Enum.join(sections, "\n\n")}
    """
  end

  defp generate_typed_query_type_and_const(resource, action, typed_query, _all_resources) do
    resource_name = build_resource_type_name(resource)

    atomized_fields = RequestedFieldsProcessor.atomize_requested_fields(typed_query.fields)

    case RequestedFieldsProcessor.process(resource, action.name, atomized_fields) do
      {:ok, {_select, _load, _template}} ->
        type_fields = format_typed_query_fields_type_for_typescript(atomized_fields)

        type_name = typed_query.ts_result_type_name
        const_name = typed_query.ts_fields_const_name

        is_array = action.type == :read && !action.get?

        result_type =
          if is_array do
            "Array<InferResult<#{resource_name}ResourceSchema, #{type_fields}>>"
          else
            "InferResult<#{resource_name}ResourceSchema, #{type_fields}>"
          end

        const_fields = format_typed_query_fields_const_for_typescript(atomized_fields)

        """
        // Type for #{typed_query.name}
        export type #{type_name} = #{result_type};

        // Field selection for #{typed_query.name} - use with RPC actions for refetching
        export const #{const_name} = #{const_fields};
        """

      {:error, error} ->
        raise "Error processing typed query #{typed_query.name}: #{inspect(error)}"
    end
  end

  defp format_typed_query_fields_const_for_typescript(fields) do
    "[" <> format_fields_const_array(fields) <> "]"
  end

  defp format_typed_query_fields_type_for_typescript(fields) do
    "[" <> format_fields_type_array(fields) <> "]"
  end

  defp format_fields_const_array(fields) do
    fields
    |> Enum.map(&"#{format_field_item(&1)} as const")
    |> Enum.join(", ")
  end

  defp format_fields_type_array(fields) do
    fields
    |> Enum.map(&format_field_item/1)
    |> Enum.join(", ")
  end

  defp format_field_item(field) when is_atom(field) do
    ~s["#{format_field_name(field)}"]
  end

  defp format_field_item({field, nested_fields}) when is_atom(field) and is_list(nested_fields) do
    "{ #{format_field_name(field)}: [#{format_fields_type_array(nested_fields)}] }"
  end

  defp format_field_item({field, {args, nested_fields}})
       when is_atom(field) and is_map(args) and is_list(nested_fields) do
    args_json = format_args_map(args)

    "{ #{format_field_name(field)}: { #{formatted_args_field()}: #{args_json}, #{formatted_fields_field()}: [#{format_fields_type_array(nested_fields)}] } }"
  end

  defp format_field_item({field, nested_fields}) when is_atom(field) and is_map(nested_fields) do
    case nested_fields do
      %{args: args, fields: fields} ->
        args_json = format_args_map(args)

        "{ #{format_field_name(field)}: { #{formatted_args_field()}: #{args_json}, #{formatted_fields_field()}: [#{format_fields_type_array(fields)}] } }"

      _ ->
        inspect(nested_fields)
    end
  end

  defp format_field_item(%{} = field_map) do
    formatted_pairs =
      field_map
      |> Enum.map(fn {k, v} ->
        key = format_field_name(k)
        value = format_field_item(v)
        "#{key}: #{value}"
      end)
      |> Enum.join(", ")

    "{ #{formatted_pairs} }"
  end

  defp format_field_item(list) when is_list(list) do
    formatted_items =
      list
      |> Enum.map(&format_field_item/1)
      |> Enum.join(", ")

    "[#{formatted_items}]"
  end

  defp format_field_item(field), do: inspect(field)

  defp format_field_name(atom) do
    formatter = AshTypescript.Rpc.output_field_formatter()
    AshTypescript.FieldFormatter.format_field(atom, formatter)
  end

  defp format_args_map(args) do
    formatted_args =
      args
      |> Enum.map(fn {k, v} ->
        "\"#{format_field_name(k)}\": #{Jason.encode!(v)}"
      end)
      |> Enum.join(", ")

    "{ #{formatted_args} }"
  end

  defp generate_input_type(resource, action, rpc_action_name) do
    if not action_has_input?(resource, action) do
      ""
    else
      input_type_name = "#{snake_to_pascal_case(rpc_action_name)}Input"

      input_field_defs =
        case action.type do
          :read ->
            arguments = action.arguments

            if arguments != [] do
              Enum.map(arguments, fn arg ->
                optional = arg.allow_nil? || arg.default != nil

                formatted_arg_name =
                  AshTypescript.FieldFormatter.format_field(
                    arg.name,
                    AshTypescript.Rpc.output_field_formatter()
                  )

                {formatted_arg_name, get_ts_type(arg), optional}
              end)
            else
              []
            end

          :create ->
            accepts = Ash.Resource.Info.action(resource, action.name).accept || []
            arguments = action.arguments

            if accepts != [] || arguments != [] do
              accept_field_defs =
                Enum.map(accepts, fn field_name ->
                  attr = Ash.Resource.Info.attribute(resource, field_name)
                  optional = attr.allow_nil? || attr.default != nil
                  base_type = AshTypescript.Codegen.get_ts_input_type(attr)
                  field_type = if attr.allow_nil?, do: "#{base_type} | null", else: base_type

                  formatted_field_name =
                    AshTypescript.FieldFormatter.format_field(
                      field_name,
                      AshTypescript.Rpc.output_field_formatter()
                    )

                  {formatted_field_name, field_type, optional}
                end)

              argument_field_defs =
                Enum.map(arguments, fn arg ->
                  optional = arg.allow_nil? || arg.default != nil

                  formatted_arg_name =
                    AshTypescript.FieldFormatter.format_field(
                      arg.name,
                      AshTypescript.Rpc.output_field_formatter()
                    )

                  {formatted_arg_name, get_ts_type(arg), optional}
                end)

              accept_field_defs ++ argument_field_defs
            else
              []
            end

          action_type when action_type in [:update, :destroy] ->
            if action.accept != [] || action.arguments != [] do
              accept_field_defs =
                Enum.map(action.accept, fn field_name ->
                  attr = Ash.Resource.Info.attribute(resource, field_name)
                  optional = attr.allow_nil? || attr.default != nil
                  base_type = AshTypescript.Codegen.get_ts_input_type(attr)
                  field_type = if attr.allow_nil?, do: "#{base_type} | null", else: base_type

                  formatted_field_name =
                    AshTypescript.FieldFormatter.format_field(
                      field_name,
                      AshTypescript.Rpc.output_field_formatter()
                    )

                  {formatted_field_name, field_type, optional}
                end)

              argument_field_defs =
                Enum.map(action.arguments, fn arg ->
                  optional = arg.allow_nil? || arg.default != nil

                  formatted_arg_name =
                    AshTypescript.FieldFormatter.format_field(
                      arg.name,
                      AshTypescript.Rpc.output_field_formatter()
                    )

                  {formatted_arg_name, get_ts_type(arg), optional}
                end)

              accept_field_defs ++ argument_field_defs
            else
              []
            end

          :action ->
            arguments = action.arguments

            if arguments != [] do
              Enum.map(arguments, fn arg ->
                optional = arg.allow_nil? || arg.default != nil

                formatted_arg_name =
                  AshTypescript.FieldFormatter.format_field(
                    arg.name,
                    AshTypescript.Rpc.output_field_formatter()
                  )

                {formatted_arg_name, get_ts_type(arg), optional}
              end)
            else
              []
            end
        end

      field_lines =
        Enum.map(input_field_defs, fn {name, type, optional} ->
          "  #{name}#{if optional, do: "?", else: ""}: #{type};"
        end)

      """
      export type #{input_type_name} = {
      #{Enum.join(field_lines, "\n")}
      };
      """
    end
  end


  defp generate_result_type(resource, action, rpc_action_name) do
    resource_name = build_resource_type_name(resource)
    rpc_action_name_pascal = snake_to_pascal_case(rpc_action_name)

    case action.type do
      :read when action.get? ->
        """
        export type #{rpc_action_name_pascal}Fields = UnifiedFieldSelection<#{resource_name}ResourceSchema>[];

        type Infer#{rpc_action_name_pascal}Result<
          Fields extends #{rpc_action_name_pascal}Fields,
        > = InferResult<#{resource_name}ResourceSchema, Fields> | null;
        """

      :read ->
        if action_supports_pagination?(action) do
          fields_type = """
          export type #{rpc_action_name_pascal}Fields = UnifiedFieldSelection<#{resource_name}ResourceSchema>[];
          """

          pagination_type =
            generate_pagination_result_type(
              resource,
              action,
              rpc_action_name_pascal,
              resource_name
            )

          fields_type <> "\n" <> pagination_type
        else
          """
          export type #{rpc_action_name_pascal}Fields = UnifiedFieldSelection<#{resource_name}ResourceSchema>[];

          type Infer#{rpc_action_name_pascal}Result<
            Fields extends #{rpc_action_name_pascal}Fields,
          > = Array<InferResult<#{resource_name}ResourceSchema, Fields>>;
          """
        end

      action_type when action_type in [:create, :update] ->
        """
        export type #{rpc_action_name_pascal}Fields = UnifiedFieldSelection<#{resource_name}ResourceSchema>[];

        type Infer#{rpc_action_name_pascal}Result<
          Fields extends #{rpc_action_name_pascal}Fields,
        > = InferResult<#{resource_name}ResourceSchema, Fields>;
        """

      :destroy ->
        ""

      :action ->
        case action_returns_field_selectable_type?(action) do
          {:ok, type, value} when type in [:resource, :array_of_resource] ->
            target_resource_name = build_resource_type_name(value)

            if type == :array_of_resource do
              """
              export type #{rpc_action_name_pascal}Fields = UnifiedFieldSelection<#{target_resource_name}ResourceSchema>[];

              type Infer#{rpc_action_name_pascal}Result<
                Fields extends #{rpc_action_name_pascal}Fields,
              > = Array<InferResult<#{target_resource_name}ResourceSchema, Fields>>;
              """
            else
              """
              export type #{rpc_action_name_pascal}Fields = UnifiedFieldSelection<#{target_resource_name}ResourceSchema>[];

              type Infer#{rpc_action_name_pascal}Result<
                Fields extends #{rpc_action_name_pascal}Fields,
              > = InferResult<#{target_resource_name}ResourceSchema, Fields>;
              """
            end

          {:ok, type, fields} when type in [:typed_map, :array_of_typed_map] ->
            typed_map_schema = build_map_type(fields)

            if type == :array_of_typed_map do
              """
              export type #{rpc_action_name_pascal}Fields = UnifiedFieldSelection<#{typed_map_schema}>[];

              type Infer#{rpc_action_name_pascal}Result<
                Fields extends #{rpc_action_name_pascal}Fields,
              > = Array<InferResult<#{typed_map_schema}, Fields>>;
              """
            else
              """
              export type #{rpc_action_name_pascal}Fields = UnifiedFieldSelection<#{typed_map_schema}>[];

              type Infer#{rpc_action_name_pascal}Result<
                Fields extends #{rpc_action_name_pascal}Fields,
              > = InferResult<#{typed_map_schema}, Fields>;
              """
            end

          _ ->
            if action.returns do
              return_type = get_ts_type(%{type: action.returns, constraints: action.constraints})

              """
              type Infer#{rpc_action_name_pascal}Result = #{return_type};
              """
            else
              """
              type Infer#{rpc_action_name_pascal}Result = {};
              """
            end
        end
    end
  end

  defp generate_pagination_result_type(_resource, action, rpc_action_name_pascal, resource_name) do
    supports_offset = action_supports_offset_pagination?(action)
    supports_keyset = action_supports_keyset_pagination?(action)

    cond do
      supports_offset and supports_keyset ->
        generate_mixed_pagination_result_type(rpc_action_name_pascal, resource_name)

      supports_offset ->
        generate_offset_pagination_result_type(rpc_action_name_pascal, resource_name)

      supports_keyset ->
        generate_keyset_pagination_result_type(rpc_action_name_pascal, resource_name)
    end
  end

  defp generate_offset_pagination_result_type(rpc_action_name_pascal, resource_name) do
    results_field = formatted_results_field()
    has_more_field = formatted_has_more_field()
    limit_field = formatted_limit_field()
    offset_field = formatted_offset_field()

    """
    type Infer#{rpc_action_name_pascal}Result<
      Fields extends #{rpc_action_name_pascal}Fields,
    > = {
      #{results_field}: Array<InferResult<#{resource_name}ResourceSchema, Fields>>;
      #{has_more_field}: boolean;
      #{limit_field}: number;
      #{offset_field}: number;
    };
    """
  end

  defp generate_keyset_pagination_result_type(rpc_action_name_pascal, resource_name) do
    results_field = formatted_results_field()
    has_more_field = formatted_has_more_field()
    limit_field = formatted_limit_field()
    after_field = formatted_after_field()
    before_field = formatted_before_field()
    previous_page_field = formatted_previous_page_field()
    next_page_field = formatted_next_page_field()

    """
    type Infer#{rpc_action_name_pascal}Result<
      Fields extends #{rpc_action_name_pascal}Fields,
    > = {
      #{results_field}: Array<InferResult<#{resource_name}ResourceSchema, Fields>>;
      #{has_more_field}: boolean;
      #{limit_field}: number;
      #{after_field}: string | null;
      #{before_field}: string | null;
      #{previous_page_field}: string;
      #{next_page_field}: string;
    };
    """
  end

  defp generate_mixed_pagination_result_type(rpc_action_name_pascal, resource_name) do
    results_field = formatted_results_field()
    has_more_field = formatted_has_more_field()
    limit_field = formatted_limit_field()
    offset_field = formatted_offset_field()
    after_field = formatted_after_field()
    before_field = formatted_before_field()
    count_field = format_output_field(:count)
    previous_page_field = formatted_previous_page_field()
    next_page_field = formatted_next_page_field()
    type_field = format_output_field(:type)

    """
    type Infer#{rpc_action_name_pascal}Result<
      Fields extends #{rpc_action_name_pascal}Fields,
    > = {
      #{results_field}: Array<InferResult<#{resource_name}ResourceSchema, Fields>>;
      #{has_more_field}: boolean;
      #{limit_field}: number;
      #{offset_field}: number;
      #{count_field}?: number | null;
      #{type_field}: "offset";
    } | {
      #{results_field}: Array<InferResult<#{resource_name}ResourceSchema, Fields>>;
      #{has_more_field}: boolean;
      #{limit_field}: number;
      #{after_field}: string | null;
      #{before_field}: string | null;
      #{previous_page_field}: string;
      #{next_page_field}: string;
      #{count_field}?: number | null;
      #{type_field}: "keyset";
    };
    """
  end

  defp generate_pagination_config_fields(action) do
    supports_offset = action_supports_offset_pagination?(action)
    supports_keyset = action_supports_keyset_pagination?(action)
    supports_countable = action_supports_countable?(action)
    is_required = action_requires_pagination?(action)
    has_default_limit = action_has_default_limit?(action)

    if supports_offset or supports_keyset do
      optional_mark = if is_required, do: "", else: "?"
      limit_required = if is_required and not has_default_limit, do: "", else: "?"

      cond do
        supports_offset and supports_keyset ->
          generate_mixed_pagination_config_fields(
            limit_required,
            supports_countable,
            optional_mark
          )

        supports_offset ->
          generate_offset_pagination_config_fields(
            limit_required,
            supports_countable,
            optional_mark
          )

        supports_keyset ->
          generate_keyset_pagination_config_fields(limit_required, optional_mark)
      end
    else
      []
    end
  end

  defp generate_offset_pagination_config_fields(limit_required, supports_countable, optional_mark) do
    fields = [
      "    #{formatted_limit_field()}#{limit_required}: number;",
      "    #{formatted_offset_field()}?: number;"
    ]

    fields =
      if supports_countable do
        fields ++ ["    #{format_output_field(:count)}?: boolean;"]
      else
        fields
      end

    [
      "  #{formatted_page_field()}#{optional_mark}: {"
    ] ++
      fields ++
      [
        "  };"
      ]
  end

  defp generate_keyset_pagination_config_fields(limit_required, optional_mark) do
    fields = [
      "    #{formatted_limit_field()}#{limit_required}: number;",
      "    #{formatted_after_field()}?: string;",
      "    #{formatted_before_field()}?: string;"
    ]

    [
      "  #{formatted_page_field()}#{optional_mark}: {"
    ] ++
      fields ++
      [
        "  };"
      ]
  end

  defp generate_mixed_pagination_config_fields(limit_required, supports_countable, optional_mark) do
    offset_fields = [
      "      #{formatted_limit_field()}#{limit_required}: number;",
      "      #{formatted_offset_field()}?: number;"
    ]

    offset_fields =
      if supports_countable do
        offset_fields ++ ["      #{format_output_field(:count)}?: boolean;"]
      else
        offset_fields
      end

    keyset_fields = [
      "      #{formatted_limit_field()}#{limit_required}: number;",
      "      #{formatted_after_field()}?: string;",
      "      #{formatted_before_field()}?: string;"
    ]

    [
      "  #{formatted_page_field()}#{optional_mark}: ("
    ] ++
      [
        "    {"
      ] ++
      offset_fields ++
      [
        "    } | {"
      ] ++
      keyset_fields ++
      [
        "    }"
      ] ++
      [
        "  );"
      ]
  end

  defp generate_rpc_execution_function(resource, action, rpc_action_name, endpoint_process) do
    function_name =
      AshTypescript.FieldFormatter.format_field(
        rpc_action_name,
        AshTypescript.Rpc.output_field_formatter()
      )

    rpc_action_name_pascal = snake_to_pascal_case(rpc_action_name)
    input_type_name = "#{rpc_action_name_pascal}Input"
    resource_name = build_resource_type_name(resource)

    requires_tenant = AshTypescript.Rpc.requires_tenant_parameter?(resource)
    requires_primary_key = action.type in [:update, :destroy]

    supports_pagination =
      action.type == :read and not action.get? and action_supports_pagination?(action)

    supports_filtering = action.type == :read and not action.get?

    config_fields =
      if requires_tenant do
        ["  #{format_output_field(:tenant)}: string;"]
      else
        []
      end

    config_fields =
      if requires_primary_key do
        primary_key_attrs = Ash.Resource.Info.primary_key(resource)

        if Enum.count(primary_key_attrs) == 1 do
          attr_name = Enum.at(primary_key_attrs, 0)
          attr = Ash.Resource.Info.attribute(resource, attr_name)
          formatted_primary_key = format_output_field(:primary_key)
          config_fields ++ ["  #{formatted_primary_key}: #{get_ts_type(attr)};"]
        else
          formatted_primary_key = format_output_field(:primary_key)

          primary_key_def =
            [
              "  #{formatted_primary_key}: {"
            ] ++
              Enum.map(primary_key_attrs, fn attr_name ->
                attr = Ash.Resource.Info.attribute(resource, attr_name)
                formatted_attr_name = format_output_field(attr.name)
                "    #{formatted_attr_name}: #{get_ts_type(attr)};"
              end) ++
              [
                "  };"
              ]

          config_fields ++ primary_key_def
        end
      else
        config_fields
      end

    config_fields =
      if action_has_input?(resource, action) do
        config_fields ++ ["  #{format_output_field(:input)}: #{input_type_name};"]
      else
        config_fields
      end

    {config_fields, has_fields, fields_generic} =
      if action.type != :destroy do
        case action.type do
          :action ->
            case action_returns_field_selectable_type?(action) do
              {:ok, type, _value} when type in [:resource, :array_of_resource] ->
                updated_fields = config_fields ++ ["  #{formatted_fields_field()}: Fields;"]

                {updated_fields, true, "Fields extends #{rpc_action_name_pascal}Fields"}

              {:ok, type, _fields} when type in [:typed_map, :array_of_typed_map] ->
                updated_fields =
                  config_fields ++
                    [
                      "  #{formatted_fields_field()}: Fields;"
                    ]

                {updated_fields, true, "Fields extends #{rpc_action_name_pascal}Fields"}

              _ ->
                {config_fields, false, nil}
            end

          _ ->
            updated_fields = config_fields ++ ["  #{formatted_fields_field()}: Fields;"]

            {updated_fields, true, "Fields extends #{rpc_action_name_pascal}Fields"}
        end
      else
        {config_fields, false, nil}
      end

    config_fields =
      if supports_filtering do
        config_fields ++ ["  #{format_output_field(:filter)}?: #{resource_name}FilterInput;"]
      else
        config_fields
      end

    config_fields =
      if supports_filtering do
        config_fields ++ ["  #{format_output_field(:sort)}?: string;"]
      else
        config_fields
      end

    config_fields =
      if supports_pagination do
        pagination_fields = generate_pagination_config_fields(action)
        config_fields ++ pagination_fields
      else
        config_fields
      end

    config_fields = config_fields ++ ["  headers?: Record<string, string>;"]

    config_type_def = "{\n#{Enum.join(config_fields, "\n")}\n}"
    success_field = format_output_field(:success)
    errors_field = format_output_field(:errors)

    {result_type_def, return_type_def, generic_param, function_signature} =
      cond do
        action.type == :destroy ->
          result_type = """
          | { #{success_field}: true; data: {} }
          | {
              #{success_field}: false;
              #{errors_field}: Array<{
                #{formatted_error_type_field()}: string;
                #{formatted_error_message_field()}: string;
                #{formatted_error_field_path_field()}?: string;
                #{formatted_error_details_field()}: Record<string, string>;
              }>;
            }
          """

          result_type_def = "export type #{rpc_action_name_pascal}Result = #{result_type};"

          {result_type_def, "#{rpc_action_name_pascal}Result", "", "config: #{config_type_def}"}

        has_fields ->
          result_type = """
          | { #{success_field}: true; data: Infer#{rpc_action_name_pascal}Result<Fields> }
          | {
              #{success_field}: false;
              #{errors_field}: Array<{
                #{formatted_error_type_field()}: string;
                #{formatted_error_message_field()}: string;
                #{formatted_error_field_path_field()}?: string;
                #{formatted_error_details_field()}: Record<string, string>;
              }>;
            }
          """

          result_type_def =
            "export type #{rpc_action_name_pascal}Result<#{fields_generic}> = #{result_type};"

          {result_type_def, "#{rpc_action_name_pascal}Result<Fields>", "#{fields_generic}",
           "config: #{config_type_def}"}

        true ->
          result_type = """
          | { #{success_field}: true; data: Infer#{rpc_action_name_pascal}Result }
          | {
              #{success_field}: false;
              #{errors_field}: Array<{
                #{formatted_error_type_field()}: string;
                #{formatted_error_message_field()}: string;
                #{formatted_error_field_path_field()}?: string;
                #{formatted_error_details_field()}: Record<string, string>;
              }>;
            }
          """

          result_type_def = "export type #{rpc_action_name_pascal}Result = #{result_type};"

          {result_type_def, "#{rpc_action_name_pascal}Result", "", "config: #{config_type_def}"}
      end

    generic_part = if generic_param != "", do: "<#{generic_param}>", else: ""

    """
    #{result_type_def}

    export async function #{function_name}#{generic_part}(
      #{function_signature}
    ): Promise<#{return_type_def}> {
      const payload = {
        action: "#{rpc_action_name}",
        ...config
      };

      const headers: Record<string, string> = {
        "Content-Type": "application/json",
        ...config.headers,
      };

      const response = await fetch("#{endpoint_process}", {
        method: "POST",
        headers,
        body: JSON.stringify(payload),
      });

      if (!response.ok) {
        return {
          #{success_field}: false,
          #{errors_field}: [{ #{formatted_error_type_field()}: "network", #{formatted_error_message_field()}: response.statusText, #{formatted_error_details_field()}: {} }],
        };
      }

      const result = await response.json();
      return result as #{return_type_def};
    }
    """
  end

  defp generate_validation_function(resource, action, rpc_action_name, endpoint_validate) do
    function_name =
      AshTypescript.FieldFormatter.format_field(
        "validate_#{rpc_action_name}",
        AshTypescript.Rpc.output_field_formatter()
      )

    rpc_action_name_pascal = snake_to_pascal_case(rpc_action_name)

    requires_tenant = AshTypescript.Rpc.requires_tenant_parameter?(resource)
    requires_primary_key = action.type in [:update, :destroy]

    config_fields = []

    config_fields =
      if requires_tenant do
        config_fields ++ ["  #{format_output_field(:tenant)}: string;"]
      else
        config_fields
      end

    config_fields =
      if requires_primary_key do
        formatted_primary_key = format_output_field(:primary_key)
        config_fields ++ ["  #{formatted_primary_key}: string;"]
      else
        config_fields
      end

    config_fields =
      if action_has_input?(resource, action) do
        config_fields ++ ["  #{format_output_field(:input)}: #{rpc_action_name_pascal}Input;"]
      else
        config_fields
      end

    config_fields = config_fields ++ ["  headers?: Record<string, string>;"]

    config_type_def = "{\n#{Enum.join(config_fields, "\n")}\n}"

    success_field = format_output_field(:success)
    errors_field = format_output_field(:errors)
    validation_result_type = """
    export type Validate#{rpc_action_name_pascal}Result =
      | { #{success_field}: true }
      | {
          #{success_field}: false;
          #{errors_field}: Array<{
            #{formatted_error_type_field()}: string;
            #{formatted_error_message_field()}: string;
            #{format_output_field(:field)}?: string;
            #{formatted_error_field_path_field()}?: string;
            #{formatted_error_details_field()}?: Record<string, any>;
          }>;
        };
    """

    """
    #{validation_result_type}

    export async function #{function_name}(
      config: #{config_type_def}
    ): Promise<Validate#{rpc_action_name_pascal}Result> {
      const payload = {
        action: "#{rpc_action_name}",
        ...config
      };

      const headers: Record<string, string> = {
        "Content-Type": "application/json",
        ...config.headers,
      };

      const response = await fetch("#{endpoint_validate}", {
        method: "POST",
        headers,
        body: JSON.stringify(payload),
      });

      if (!response.ok) {
        return {
          #{success_field}: false,
          #{errors_field}: [{ #{formatted_error_type_field()}: "network", #{formatted_error_message_field()}: response.statusText }],
        };
      }

      const result = await response.json();
      return result as Validate#{rpc_action_name_pascal}Result;
    }
    """
  end

  defp generate_rpc_function(
         {resource, action, rpc_action},
         _resources_and_actions,
         endpoint_process,
         endpoint_validate,
         _otp_app
       ) do
    rpc_action_name = to_string(rpc_action.name)

    input_type = generate_input_type(resource, action, rpc_action_name)

    error_type = ValidationErrorSchemas.generate_validation_error_type(resource, action, rpc_action_name)

    zod_schema =
      if AshTypescript.Rpc.generate_zod_schemas?() do
        ZodSchemaGenerator.generate_zod_schema(resource, action, rpc_action_name)
      else
        ""
      end

    result_type = generate_result_type(resource, action, rpc_action_name)

    rpc_function =
      generate_rpc_execution_function(resource, action, rpc_action_name, endpoint_process)

    validation_function =
      generate_validation_function(resource, action, rpc_action_name, endpoint_validate)

    functions_section =
      """
      #{rpc_function}

      #{validation_function}
      """

    base_types = [input_type, error_type] |> Enum.reject(&(&1 == ""))

    output_parts =
      if zod_schema != "" do
        base_types ++ [zod_schema, result_type, functions_section]
      else
        base_types ++ [result_type, functions_section]
      end

    Enum.join(output_parts, "\n")
    |> String.trim_trailing("\n")
    |> then(&(&1 <> "\n"))
  end
end
