# This file is auto-generated by generate.exs — DO NOT EDIT MANUALLY.
# Re-generate with: mix run generate.exs

defmodule Firecrawl do
  @moduledoc """
  Auto-generated Firecrawl API v2 client.

  Generated from the OpenAPI spec at:
  https://raw.githubusercontent.com/firecrawl/firecrawl-docs/main/api-reference/v2-openapi.json

  ## Configuration

  Set your API key in application config:

      config :firecrawl, api_key: "fc-your-api-key"

  Or pass it as an option to any function:

      Firecrawl.scrape_and_extract_from_url([url: "https://example.com"], api_key: "fc-your-api-key")

  ## Options

  All functions accept an optional keyword list as the last argument with:

    * `:api_key` - Override the API key for this request
    * `:base_url` - Override the default API base URL (useful for self-hosted instances)
    * Any other options are passed through to the `Req` request

  ## Usage

      {:ok, response} = Firecrawl.scrape_and_extract_from_url(
        url: "https://example.com"
      )

      {:ok, response} = Firecrawl.crawl_urls(
        url: "https://example.com",
        limit: 100
      )
  """

  @type response :: {:ok, Req.Response.t()} | {:error, Exception.t() | Firecrawl.Error.t()}

  @base_url "https://api.firecrawl.dev/v2"

  defp client(opts) do
    api_key =
      Keyword.get_lazy(opts, :api_key, fn ->
        Application.get_env(:firecrawl, :api_key)
      end)

    api_key =
      case api_key do
        key when is_binary(key) ->
          case String.trim(key) do
            "" ->
              raise """
              Firecrawl API key not found or empty. Set it in your config:

                  config :firecrawl, api_key: "fc-your-api-key"

              Or pass it as an option:

                  Firecrawl.scrape_and_extract_from_url([url: "..."], api_key: "fc-your-api-key")
              """

            trimmed ->
              trimmed
          end

        _ ->
          raise """
          Firecrawl API key not found or empty. Set it in your config:

              config :firecrawl, api_key: "fc-your-api-key"

          Or pass it as an option:

              Firecrawl.scrape_and_extract_from_url([url: "..."], api_key: "fc-your-api-key")
          """
      end

    {base_url, opts} = Keyword.pop(opts, :base_url, @base_url)
    opts = Keyword.delete(opts, :api_key)

    Req.new(
      base_url: base_url,
      headers: [{"authorization", "Bearer #{api_key}"}]
    )
    |> Req.merge(opts)
    |> Req.Request.append_response_steps(firecrawl_error_handler: &handle_api_error/1)
  end

  defp handle_api_error({request, %Req.Response{status: status} = response}) when status >= 400 do
    {request, Firecrawl.Error.exception(status: status, body: response.body)}
  end

  defp handle_api_error({request, response}), do: {request, response}

  defp to_body(validated_params, key_mapping) do
    Map.new(validated_params, fn {k, v} ->
      json_key = Map.fetch!(key_mapping, k)
      {json_key, to_json_value(v)}
    end)
  end

  defp to_query(validated_params, key_mapping) do
    Enum.map(validated_params, fn {k, v} ->
      {Map.fetch!(key_mapping, k), to_json_value(v)}
    end)
  end

  defp to_json_value(atom) when is_atom(atom) and atom not in [true, false, nil] do
    Atom.to_string(atom)
  end

  defp to_json_value(map) when is_map(map), do: map

  defp to_json_value(list) when is_list(list) do
    if Keyword.keyword?(list) and list != [] do
      Map.new(list, fn {k, v} -> {camelize(Atom.to_string(k)), to_json_value(v)} end)
    else
      Enum.map(list, &to_json_value/1)
    end
  end

  defp to_json_value(value), do: value

  defp camelize(string) do
    [first | rest] = String.split(string, "_")
    Enum.join([first | Enum.map(rest, &String.capitalize/1)])
  end

  defp fetch_file_field(file, key) do
    case Keyword.fetch(file, key) do
      {:ok, _value} = ok -> ok
      :error -> {:error, %ArgumentError{message: "missing required file field: #{key}"}}
    end
  end

  defp validate_filename(filename) do
    if is_binary(filename) and filename != "" do
      :ok
    else
      {:error, %ArgumentError{message: "filename cannot be empty"}}
    end
  end

  defp validate_data(data) do
    if is_nil(data) do
      {:error, %ArgumentError{message: "file data cannot be empty"}}
    else
      :ok
    end
  end

  @doc """
  Cancel an agent job

  `DELETE /agent/{jobId}`

  Tag: Agent

  ## Path Parameters

    * `job_id` - Path parameter `jobId`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec cancel_agent(String.t(), keyword()) :: response()
  def cancel_agent(job_id, opts \\ []) do
    Req.delete(client(opts), url: "/agent/#{job_id}")
  end


  @doc """
  Bang variant of `cancel_agent`. Raises on error.
  """
  @spec cancel_agent!(String.t(), keyword()) :: Req.Response.t()
  def cancel_agent!(job_id, opts \\ []) do
    Req.delete!(client(opts), url: "/agent/#{job_id}")
  end


  @doc """
  Cancel a batch scrape job

  `DELETE /batch/scrape/{id}`

  Tag: Scraping

  ## Path Parameters

    * `id` - Path parameter `id`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec cancel_batch_scrape(String.t(), keyword()) :: response()
  def cancel_batch_scrape(id, opts \\ []) do
    Req.delete(client(opts), url: "/batch/scrape/#{id}")
  end


  @doc """
  Bang variant of `cancel_batch_scrape`. Raises on error.
  """
  @spec cancel_batch_scrape!(String.t(), keyword()) :: Req.Response.t()
  def cancel_batch_scrape!(id, opts \\ []) do
    Req.delete!(client(opts), url: "/batch/scrape/#{id}")
  end


  @doc """
  Cancel a crawl job

  `DELETE /crawl/{id}`

  Tag: Crawling

  ## Path Parameters

    * `id` - Path parameter `id`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec cancel_crawl(String.t(), keyword()) :: response()
  def cancel_crawl(id, opts \\ []) do
    Req.delete(client(opts), url: "/crawl/#{id}")
  end


  @doc """
  Bang variant of `cancel_crawl`. Raises on error.
  """
  @spec cancel_crawl!(String.t(), keyword()) :: Req.Response.t()
  def cancel_crawl!(id, opts \\ []) do
    Req.delete!(client(opts), url: "/crawl/#{id}")
  end


  @crawl_params_preview_schema NimbleOptions.new!([
    prompt: [type: :string, required: true, doc: "Natural language prompt describing what you want to crawl"],
    url: [type: :string, required: true, doc: "The URL to crawl"]
  ])

  @crawl_params_preview_key_mapping %{prompt: "prompt", url: "url"}

  @doc """
  Preview crawl parameters generated from natural language prompt

  `POST /crawl/params-preview`

  Tag: Crawling

  ## Parameters

  Validated by `NimbleOptions`. Pass params as a keyword list with snake_case keys.
  See `@crawl_params_preview_schema` for the full schema.

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec crawl_params_preview(keyword(), keyword()) :: response()
  def crawl_params_preview(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @crawl_params_preview_schema) do
      Req.post(client(opts), url: "/crawl/params-preview", json: to_body(params, @crawl_params_preview_key_mapping))
    end
  end


  @doc """
  Bang variant of `crawl_params_preview`. Raises on error.
  """
  @spec crawl_params_preview!(keyword(), keyword()) :: Req.Response.t()
  def crawl_params_preview!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @crawl_params_preview_schema)
    Req.post!(client(opts), url: "/crawl/params-preview", json: to_body(params, @crawl_params_preview_key_mapping))
  end


  @crawl_urls_schema NimbleOptions.new!([
    allow_external_links: [type: :boolean, doc: "Allows the crawler to follow links to external websites."],
    allow_subdomains: [type: :boolean, doc: "Allows the crawler to follow links to subdomains of the main domain."],
    crawl_entire_domain: [type: :boolean, doc: "Allows the crawler to follow internal links to sibling or parent URLs, not just child paths.\n\nfalse: Only crawls deeper (child) URLs.\n→ e.g. /features/feature-1 → /features/feature-1/tips ✅\n→ Won't follow /pricing or / ❌\n\ntrue: Crawls any internal links, including siblings and parents.\n→ e.g. /features/feature-1 → /pricing, /, etc. ✅\n\nUse true for broader internal coverage beyond nested paths."],
    delay: [type: {:or, [:integer, :float]}, doc: "Delay in seconds between scrapes. This helps respect website rate limits. Setting this forces concurrency to 1."],
    exclude_paths: [type: {:list, :string}, doc: "URL pathname regex patterns that exclude matching URLs from the crawl. For example, if you set \"excludePaths\": [\"blog/.*\"] for the base URL firecrawl.dev, any results matching that pattern will be excluded, such as https://www.firecrawl.dev/blog/firecrawl-launch-week-1-recap."],
    ignore_query_parameters: [type: :boolean, doc: "Do not re-scrape the same path with different (or none) query parameters"],
    include_paths: [type: {:list, :string}, doc: "URL pathname regex patterns that include matching URLs in the crawl. Only the paths that match the specified patterns will be included in the response. Note: the starting URL is also checked against these patterns — if it does not match, the crawl may return 0 pages. For example, if you set \"includePaths\": [\"blog/.*\"] for the base URL firecrawl.dev/blog, only pages under /blog/ will be included in the results, such as https://www.firecrawl.dev/blog/firecrawl-launch-week-1-recap."],
    limit: [type: :integer, doc: "Maximum number of pages to crawl. Default limit is 10000."],
    max_concurrency: [type: :integer, doc: "Maximum number of concurrent scrapes. This parameter allows you to set a concurrency limit for this crawl. If not specified, the crawl adheres to your team's concurrency limit."],
    max_discovery_depth: [type: :integer, doc: "Maximum depth to crawl based on discovery order. The root site and sitemapped pages has a discovery depth of 0. For example, if you set it to 1, and you set `sitemap: 'skip'`, you will only crawl the entered URL and all URLs that are linked on that page."],
    prompt: [type: :string, doc: "A prompt to use to generate the crawler options (all the parameters below) from natural language. Explicitly set parameters will override the generated equivalents."],
    regex_on_full_url: [type: :boolean, doc: "When true, includePaths and excludePaths regex patterns are matched against the full URL (including query parameters) instead of just the URL pathname. Useful when you need to filter URLs based on query strings."],
    scrape_options: [type: :keyword_list],
    sitemap: [type: {:or, [{:in, [:skip, :include, :only]}, :string]}, doc: "Sitemap mode when crawling. If you set it to 'skip', the crawler will ignore the website sitemap and only crawl the entered URL and discover pages from there onwards. If you set it to 'only', the crawler will only crawl URLs from the sitemap (plus the start URL) and will not discover links from HTML."],
    url: [type: :string, required: true, doc: "The base URL to start crawling from"],
    webhook: [type: :keyword_list, doc: "A webhook specification object."],
    zero_data_retention: [type: :boolean, doc: "If true, this will enable zero data retention for this crawl. To enable this feature, please contact help@firecrawl.dev"]
  ])

  @crawl_urls_key_mapping %{allow_external_links: "allowExternalLinks", allow_subdomains: "allowSubdomains", crawl_entire_domain: "crawlEntireDomain", delay: "delay", exclude_paths: "excludePaths", ignore_query_parameters: "ignoreQueryParameters", include_paths: "includePaths", limit: "limit", max_concurrency: "maxConcurrency", max_discovery_depth: "maxDiscoveryDepth", prompt: "prompt", regex_on_full_url: "regexOnFullURL", scrape_options: "scrapeOptions", sitemap: "sitemap", url: "url", webhook: "webhook", zero_data_retention: "zeroDataRetention"}

  @doc """
  Crawl multiple URLs based on options

  `POST /crawl`

  Tag: Crawling

  ## Parameters

  Validated by `NimbleOptions`. Pass params as a keyword list with snake_case keys.
  See `@crawl_urls_schema` for the full schema.

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec crawl_urls(keyword(), keyword()) :: response()
  def crawl_urls(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @crawl_urls_schema) do
      Req.post(client(opts), url: "/crawl", json: to_body(params, @crawl_urls_key_mapping))
    end
  end


  @doc """
  Bang variant of `crawl_urls`. Raises on error.
  """
  @spec crawl_urls!(keyword(), keyword()) :: Req.Response.t()
  def crawl_urls!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @crawl_urls_schema)
    Req.post!(client(opts), url: "/crawl", json: to_body(params, @crawl_urls_key_mapping))
  end


  @create_browser_session_schema NimbleOptions.new!([
    activity_ttl: [type: :integer, doc: "Time in seconds before the session is destroyed due to inactivity"],
    profile: [type: :keyword_list, doc: "Enable persistent storage across browser sessions. Data saved in one session can be loaded in a later session using the same name."],
    stream_web_view: [type: :boolean, doc: "Whether to stream a live view of the browser"],
    ttl: [type: :integer, doc: "Total time-to-live in seconds for the browser session"]
  ])

  @create_browser_session_key_mapping %{activity_ttl: "activityTtl", profile: "profile", stream_web_view: "streamWebView", ttl: "ttl"}

  @doc """
  Create a browser session

  `POST /browser`

  Tag: Browser

  ## Parameters

  Validated by `NimbleOptions`. Pass params as a keyword list with snake_case keys.
  See `@create_browser_session_schema` for the full schema.

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec create_browser_session(keyword(), keyword()) :: response()
  def create_browser_session(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @create_browser_session_schema) do
      Req.post(client(opts), url: "/browser", json: to_body(params, @create_browser_session_key_mapping))
    end
  end


  @doc """
  Bang variant of `create_browser_session`. Raises on error.
  """
  @spec create_browser_session!(keyword(), keyword()) :: Req.Response.t()
  def create_browser_session!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @create_browser_session_schema)
    Req.post!(client(opts), url: "/browser", json: to_body(params, @create_browser_session_key_mapping))
  end


  @doc """
  Delete a browser session

  `DELETE /browser/{sessionId}`

  Tag: Browser

  ## Path Parameters

    * `session_id` - Path parameter `sessionId`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec delete_browser_session(String.t(), keyword()) :: response()
  def delete_browser_session(session_id, opts \\ []) do
    Req.delete(client(opts), url: "/browser/#{session_id}")
  end


  @doc """
  Bang variant of `delete_browser_session`. Raises on error.
  """
  @spec delete_browser_session!(String.t(), keyword()) :: Req.Response.t()
  def delete_browser_session!(session_id, opts \\ []) do
    Req.delete!(client(opts), url: "/browser/#{session_id}")
  end


  @execute_browser_code_schema NimbleOptions.new!([
    code: [type: :string, required: true, doc: "Code to execute in the browser sandbox"],
    language: [type: {:in, [:python, :node, :bash]}, doc: "Language of the code to execute. Use `node` for JavaScript or `bash` for agent-browser CLI commands."],
    timeout: [type: :integer, doc: "Execution timeout in seconds"]
  ])

  @execute_browser_code_key_mapping %{code: "code", language: "language", timeout: "timeout"}

  @doc """
  Execute code in a browser session

  `POST /browser/{sessionId}/execute`

  Tag: Browser

  ## Path Parameters

    * `session_id` - Path parameter `sessionId`

  ## Parameters

  Validated by `NimbleOptions`. Pass params as a keyword list with snake_case keys.
  See `@execute_browser_code_schema` for the full schema.

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec execute_browser_code(String.t(), keyword(), keyword()) :: response()
  def execute_browser_code(session_id, params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @execute_browser_code_schema) do
      Req.post(client(opts), url: "/browser/#{session_id}/execute", json: to_body(params, @execute_browser_code_key_mapping))
    end
  end


  @doc """
  Bang variant of `execute_browser_code`. Raises on error.
  """
  @spec execute_browser_code!(String.t(), keyword(), keyword()) :: Req.Response.t()
  def execute_browser_code!(session_id, params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @execute_browser_code_schema)
    Req.post!(client(opts), url: "/browser/#{session_id}/execute", json: to_body(params, @execute_browser_code_key_mapping))
  end


  @doc """
  Get all active crawls for the authenticated team

  `GET /crawl/active`

  Tag: Crawling

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec get_active_crawls(keyword()) :: response()
  def get_active_crawls(opts \\ []) do
    Req.get(client(opts), url: "/crawl/active")
  end


  @doc """
  Bang variant of `get_active_crawls`. Raises on error.
  """
  @spec get_active_crawls!(keyword()) :: Req.Response.t()
  def get_active_crawls!(opts \\ []) do
    Req.get!(client(opts), url: "/crawl/active")
  end


  @get_activity_query_schema NimbleOptions.new!([
    endpoint: [type: {:in, [:scrape, :crawl, :batch_scrape, :search, :extract, :llmstxt, :deep_research, :map, :agent, :browser, :interact]}, doc: "Filter by endpoint"],
    limit: [type: :integer, doc: "Maximum number of results per page"],
    cursor: [type: :string, doc: "Cursor for pagination. Use the cursor value from the previous response."]
  ])

  @get_activity_query_key_mapping %{endpoint: "endpoint", limit: "limit", cursor: "cursor"}

  @doc """
  List recent API activity

  `GET /team/activity`

  Tag: Account

  ## Query Parameters

    * `endpoint` — query parameter `endpoint`
    * `limit` — query parameter `limit`
    * `cursor` — query parameter `cursor`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec get_activity(keyword(), keyword()) :: response()
  def get_activity(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @get_activity_query_schema) do
      Req.get(client(opts), url: "/team/activity", params: to_query(params, @get_activity_query_key_mapping))
    end
  end


  @doc """
  Bang variant of `get_activity`. Raises on error.
  """
  @spec get_activity!(keyword(), keyword()) :: Req.Response.t()
  def get_activity!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @get_activity_query_schema)
    Req.get!(client(opts), url: "/team/activity", params: to_query(params, @get_activity_query_key_mapping))
  end


  @doc """
  Get the status of an agent job

  `GET /agent/{jobId}`

  Tag: Agent

  ## Path Parameters

    * `job_id` - Path parameter `jobId`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec get_agent_status(String.t(), keyword()) :: response()
  def get_agent_status(job_id, opts \\ []) do
    Req.get(client(opts), url: "/agent/#{job_id}")
  end


  @doc """
  Bang variant of `get_agent_status`. Raises on error.
  """
  @spec get_agent_status!(String.t(), keyword()) :: Req.Response.t()
  def get_agent_status!(job_id, opts \\ []) do
    Req.get!(client(opts), url: "/agent/#{job_id}")
  end


  @doc """
  Get the errors of a batch scrape job

  `GET /batch/scrape/{id}/errors`

  Tag: Scraping

  ## Path Parameters

    * `id` - Path parameter `id`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec get_batch_scrape_errors(String.t(), keyword()) :: response()
  def get_batch_scrape_errors(id, opts \\ []) do
    Req.get(client(opts), url: "/batch/scrape/#{id}/errors")
  end


  @doc """
  Bang variant of `get_batch_scrape_errors`. Raises on error.
  """
  @spec get_batch_scrape_errors!(String.t(), keyword()) :: Req.Response.t()
  def get_batch_scrape_errors!(id, opts \\ []) do
    Req.get!(client(opts), url: "/batch/scrape/#{id}/errors")
  end


  @doc """
  Get the status of a batch scrape job

  `GET /batch/scrape/{id}`

  Tag: Scraping

  ## Path Parameters

    * `id` - Path parameter `id`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec get_batch_scrape_status(String.t(), keyword()) :: response()
  def get_batch_scrape_status(id, opts \\ []) do
    Req.get(client(opts), url: "/batch/scrape/#{id}")
  end


  @doc """
  Bang variant of `get_batch_scrape_status`. Raises on error.
  """
  @spec get_batch_scrape_status!(String.t(), keyword()) :: Req.Response.t()
  def get_batch_scrape_status!(id, opts \\ []) do
    Req.get!(client(opts), url: "/batch/scrape/#{id}")
  end


  @doc """
  Get the errors of a crawl job

  `GET /crawl/{id}/errors`

  Tag: Crawling

  ## Path Parameters

    * `id` - Path parameter `id`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec get_crawl_errors(String.t(), keyword()) :: response()
  def get_crawl_errors(id, opts \\ []) do
    Req.get(client(opts), url: "/crawl/#{id}/errors")
  end


  @doc """
  Bang variant of `get_crawl_errors`. Raises on error.
  """
  @spec get_crawl_errors!(String.t(), keyword()) :: Req.Response.t()
  def get_crawl_errors!(id, opts \\ []) do
    Req.get!(client(opts), url: "/crawl/#{id}/errors")
  end


  @doc """
  Get the status of a crawl job

  `GET /crawl/{id}`

  Tag: Crawling

  ## Path Parameters

    * `id` - Path parameter `id`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec get_crawl_status(String.t(), keyword()) :: response()
  def get_crawl_status(id, opts \\ []) do
    Req.get(client(opts), url: "/crawl/#{id}")
  end


  @doc """
  Bang variant of `get_crawl_status`. Raises on error.
  """
  @spec get_crawl_status!(String.t(), keyword()) :: Req.Response.t()
  def get_crawl_status!(id, opts \\ []) do
    Req.get!(client(opts), url: "/crawl/#{id}")
  end


  @doc """
  Get remaining credits for the authenticated team

  `GET /team/credit-usage`

  Tag: Billing

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec get_credit_usage(keyword()) :: response()
  def get_credit_usage(opts \\ []) do
    Req.get(client(opts), url: "/team/credit-usage")
  end


  @doc """
  Bang variant of `get_credit_usage`. Raises on error.
  """
  @spec get_credit_usage!(keyword()) :: Req.Response.t()
  def get_credit_usage!(opts \\ []) do
    Req.get!(client(opts), url: "/team/credit-usage")
  end


  @get_historical_credit_usage_query_schema NimbleOptions.new!([
    by_api_key: [type: :boolean, doc: "Get historical credit usage by API key"]
  ])

  @get_historical_credit_usage_query_key_mapping %{by_api_key: "byApiKey"}

  @doc """
  Get historical credit usage for the authenticated team

  `GET /team/credit-usage/historical`

  Tag: Billing

  ## Query Parameters

    * `by_api_key` — query parameter `byApiKey`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec get_historical_credit_usage(keyword(), keyword()) :: response()
  def get_historical_credit_usage(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @get_historical_credit_usage_query_schema) do
      Req.get(client(opts), url: "/team/credit-usage/historical", params: to_query(params, @get_historical_credit_usage_query_key_mapping))
    end
  end


  @doc """
  Bang variant of `get_historical_credit_usage`. Raises on error.
  """
  @spec get_historical_credit_usage!(keyword(), keyword()) :: Req.Response.t()
  def get_historical_credit_usage!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @get_historical_credit_usage_query_schema)
    Req.get!(client(opts), url: "/team/credit-usage/historical", params: to_query(params, @get_historical_credit_usage_query_key_mapping))
  end


  @doc """
  Metrics about your team's scrape queue

  `GET /team/queue-status`

  Tag: Miscellaneous

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec get_queue_status(keyword()) :: response()
  def get_queue_status(opts \\ []) do
    Req.get(client(opts), url: "/team/queue-status")
  end


  @doc """
  Bang variant of `get_queue_status`. Raises on error.
  """
  @spec get_queue_status!(keyword()) :: Req.Response.t()
  def get_queue_status!(opts \\ []) do
    Req.get!(client(opts), url: "/team/queue-status")
  end


  @interact_with_scrape_browser_session_schema NimbleOptions.new!([
    code: [type: :string, required: true, doc: "Code to execute in the scrape-bound browser sandbox"],
    language: [type: {:in, [:python, :node, :bash]}, doc: "Language of the code to execute. Use `node` for JavaScript or `bash` for agent-browser CLI commands."],
    origin: [type: :string, doc: "Optional origin label used for execution telemetry"],
    timeout: [type: :integer, doc: "Execution timeout in seconds"]
  ])

  @interact_with_scrape_browser_session_key_mapping %{code: "code", language: "language", origin: "origin", timeout: "timeout"}

  @doc """
  Interact with the browser session associated with a scrape job

  `POST /scrape/{jobId}/interact`

  Tag: Scraping

  ## Path Parameters

    * `job_id` - Path parameter `jobId`

  ## Parameters

  Validated by `NimbleOptions`. Pass params as a keyword list with snake_case keys.
  See `@interact_with_scrape_browser_session_schema` for the full schema.

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec interact_with_scrape_browser_session(String.t(), keyword(), keyword()) :: response()
  def interact_with_scrape_browser_session(job_id, params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @interact_with_scrape_browser_session_schema) do
      Req.post(client(opts), url: "/scrape/#{job_id}/interact", json: to_body(params, @interact_with_scrape_browser_session_key_mapping))
    end
  end


  @doc """
  Bang variant of `interact_with_scrape_browser_session`. Raises on error.
  """
  @spec interact_with_scrape_browser_session!(String.t(), keyword(), keyword()) :: Req.Response.t()
  def interact_with_scrape_browser_session!(job_id, params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @interact_with_scrape_browser_session_schema)
    Req.post!(client(opts), url: "/scrape/#{job_id}/interact", json: to_body(params, @interact_with_scrape_browser_session_key_mapping))
  end


  @list_browser_sessions_query_schema NimbleOptions.new!([
    status: [type: {:in, [:active, :destroyed]}, doc: "Filter sessions by status"]
  ])

  @list_browser_sessions_query_key_mapping %{status: "status"}

  @doc """
  List browser sessions

  `GET /browser`

  Tag: Browser

  ## Query Parameters

    * `status` — query parameter `status`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec list_browser_sessions(keyword(), keyword()) :: response()
  def list_browser_sessions(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @list_browser_sessions_query_schema) do
      Req.get(client(opts), url: "/browser", params: to_query(params, @list_browser_sessions_query_key_mapping))
    end
  end


  @doc """
  Bang variant of `list_browser_sessions`. Raises on error.
  """
  @spec list_browser_sessions!(keyword(), keyword()) :: Req.Response.t()
  def list_browser_sessions!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @list_browser_sessions_query_schema)
    Req.get!(client(opts), url: "/browser", params: to_query(params, @list_browser_sessions_query_key_mapping))
  end


  @map_urls_schema NimbleOptions.new!([
    ignore_cache: [type: :boolean, doc: "Bypass the sitemap cache to retrieve fresh URLs. Sitemap data is cached for up to 7 days; use this parameter when your sitemap has been recently updated."],
    ignore_query_parameters: [type: :boolean, doc: "Do not return URLs with query parameters"],
    include_subdomains: [type: :boolean, doc: "Include subdomains of the website"],
    limit: [type: :integer, doc: "Maximum number of links to return"],
    location: [type: :keyword_list, doc: "Location settings for the request. When specified, this will use an appropriate proxy if available and emulate the corresponding language and timezone settings. Defaults to 'US' if not specified."],
    search: [type: :string, doc: "Specify a search query to order the results by relevance. Example: 'blog' will return URLs that contain the word 'blog' in the URL ordered by relevance."],
    sitemap: [type: {:or, [{:in, [:skip, :include, :only]}, :string]}, doc: "Sitemap mode when mapping. If you set it to `skip`, the sitemap won't be used to find URLs. If you set it to `only`, only URLs that are in the sitemap will be returned. By default (`include`), the sitemap and other methods will be used together to find URLs."],
    timeout: [type: :integer, doc: "Timeout in milliseconds. There is no timeout by default."],
    url: [type: :string, required: true, doc: "The base URL to start crawling from"]
  ])

  @map_urls_key_mapping %{ignore_cache: "ignoreCache", ignore_query_parameters: "ignoreQueryParameters", include_subdomains: "includeSubdomains", limit: "limit", location: "location", search: "search", sitemap: "sitemap", timeout: "timeout", url: "url"}

  @doc """
  Map multiple URLs based on options

  `POST /map`

  Tag: Mapping

  ## Parameters

  Validated by `NimbleOptions`. Pass params as a keyword list with snake_case keys.
  See `@map_urls_schema` for the full schema.

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec map_urls(keyword(), keyword()) :: response()
  def map_urls(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @map_urls_schema) do
      Req.post(client(opts), url: "/map", json: to_body(params, @map_urls_key_mapping))
    end
  end


  @doc """
  Bang variant of `map_urls`. Raises on error.
  """
  @spec map_urls!(keyword(), keyword()) :: Req.Response.t()
  def map_urls!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @map_urls_schema)
    Req.post!(client(opts), url: "/map", json: to_body(params, @map_urls_key_mapping))
  end


  @parse_file_schema NimbleOptions.new!([
    block_ads: [type: :boolean, doc: "Enable ad and cookie popup blocking."],
    exclude_tags: [type: {:list, :string}, doc: "Tags to exclude from the output."],
    formats: [type: {:list, :any}, doc: "Output formats supported for `/parse` uploads. Browser-rendering formats and change tracking are not supported."],
    headers: [type: :any, doc: "Headers to send when additional network requests are required."],
    include_tags: [type: {:list, :string}, doc: "Tags to include in the output."],
    integration: [type: :string, doc: "Optional integration identifier."],
    only_main_content: [type: :boolean, doc: "Only return the main content of the page excluding headers, navs, footers, etc."],
    origin: [type: :string, doc: "Origin identifier for analytics and logging."],
    parsers: [type: {:list, :any}, doc: "Controls file parser behavior when relevant (for example PDF parser mode)."],
    proxy: [type: {:or, [{:in, [:basic, :auto]}, :string]}, doc: "Proxy mode for parse uploads. `/parse` supports only `basic` and `auto`."],
    remove_base64_images: [type: :boolean, doc: "Remove base64-encoded images from output and keep alt text placeholders."],
    skip_tls_verification: [type: :boolean, doc: "Skip TLS certificate verification when making requests."],
    timeout: [type: :integer, doc: "Timeout in milliseconds for the request. Default is 30000 (30 seconds). Maximum is 300000 (300 seconds)."],
    zero_data_retention: [type: :boolean, doc: "If true, this will enable zero data retention for this parse. To enable this feature, please contact help@firecrawl.dev"]
  ])

  @parse_file_key_mapping %{block_ads: "blockAds", exclude_tags: "excludeTags", formats: "formats", headers: "headers", include_tags: "includeTags", integration: "integration", only_main_content: "onlyMainContent", origin: "origin", parsers: "parsers", proxy: "proxy", remove_base64_images: "removeBase64Images", skip_tls_verification: "skipTlsVerification", timeout: "timeout", zero_data_retention: "zeroDataRetention"}

  @doc """
  Upload and parse a file

  `POST /parse`

  Sends a `multipart/form-data` request.

  Tag: Scraping

  ## File

  Pass `file` as a keyword list:

    * `:filename` (required) - The filename to send.
    * `:data` (required) - The file contents as a binary.
    * `:content_type` (optional) - The MIME type of the file.

  ## Parameters

  Validated by `NimbleOptions`. Pass options as a keyword list with snake_case keys.
  These are JSON-encoded and sent as the `options` multipart field.
  See `@parse_file_schema` for the full schema.

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec parse_file(keyword(), keyword(), keyword()) :: response()
  def parse_file(file, params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @parse_file_schema),
         {:ok, filename} <- fetch_file_field(file, :filename),
         :ok <- validate_filename(filename),
         {:ok, data} <- fetch_file_field(file, :data),
         :ok <- validate_data(data) do
      content_type = Keyword.get(file, :content_type)

      file_part =
        case content_type do
          nil -> {data, filename: filename}
          ct -> {data, filename: filename, content_type: ct}
        end

      multipart = [{"options", Jason.encode!(to_body(params, @parse_file_key_mapping))}, {"file", file_part}]

      Req.post(client(opts), url: "/parse", form_multipart: multipart)
    end
  end


  @doc """
  Bang variant of `parse_file`. Raises on error.
  """
  @spec parse_file!(keyword(), keyword(), keyword()) :: Req.Response.t()
  def parse_file!(file, params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @parse_file_schema)
    filename = Keyword.fetch!(file, :filename)
    data = Keyword.fetch!(file, :data)
    content_type = Keyword.get(file, :content_type)

    if not is_binary(filename) or filename == "" do
      raise ArgumentError, "filename cannot be empty"
    end

    if is_nil(data) do
      raise ArgumentError, "file data cannot be empty"
    end

    file_part =
      case content_type do
        nil -> {data, filename: filename}
        ct -> {data, filename: filename, content_type: ct}
      end

    multipart = [{"options", Jason.encode!(to_body(params, @parse_file_key_mapping))}, {"file", file_part}]

    Req.post!(client(opts), url: "/parse", form_multipart: multipart)
  end


  @scrape_and_extract_from_url_schema NimbleOptions.new!([
    url: [type: :string, required: true, doc: "The URL to scrape"],
    actions: [type: {:list, :any}, doc: "Actions to perform on the page before grabbing the content"],
    block_ads: [type: :boolean, doc: "Enables ad-blocking and cookie popup blocking."],
    exclude_tags: [type: {:list, :string}, doc: "Tags to exclude from the output."],
    formats: [type: {:list, :any}, doc: "Output formats to include in the response. You can specify one or more formats, either as strings (e.g., `'markdown'`) or as objects with additional options (e.g., `{ type: 'json', schema: {...} }`, `{ type: 'question', question: '...' }`, `{ type: 'highlights', query: '...' }`). The legacy `{ type: 'query', prompt: '...', mode: 'freeform' | 'directQuote' }` format is deprecated."],
    headers: [type: :any, doc: "Headers to send with the request. Can be used to send cookies, user-agent, etc."],
    include_tags: [type: {:list, :string}, doc: "Tags to include in the output."],
    location: [type: :keyword_list, doc: "Location settings for the request. When specified, this will use an appropriate proxy if available and emulate the corresponding language and timezone settings. Defaults to 'US' if not specified."],
    max_age: [type: :integer, doc: "Returns a cached version of the page if it is younger than this age in milliseconds. If a cached version of the page is older than this value, the page will be scraped. If you do not need extremely fresh data, enabling this can speed up your scrapes by 500%. Defaults to 2 days."],
    min_age: [type: :integer, doc: "When set, the request only checks the cache and never triggers a fresh scrape. The value is in milliseconds and specifies the minimum age the cached data must be. If matching cached data exists, it is returned instantly. If no cached data is found, a 404 with error code SCRAPE_NO_CACHED_DATA is returned. Set to 1 to accept any cached data regardless of age."],
    mobile: [type: :boolean, doc: "Set to true if you want to emulate scraping from a mobile device. Useful for testing responsive pages and taking mobile screenshots."],
    only_main_content: [type: :boolean, doc: "Only return the main content of the page excluding headers, navs, footers, etc."],
    parsers: [type: {:list, :any}, doc: "Controls how files are processed during scraping. When \"pdf\" is included (default), the PDF content is extracted and converted to markdown format, with billing based on the number of pages (1 credit per page). When an empty array is passed, the PDF file is returned in base64 encoding with a flat rate of 1 credit for the entire PDF."],
    profile: [type: :keyword_list, doc: "Enable persistent browser storage across scrape and interact sessions. Pass a profile when scraping to preserve cookies, localStorage, and session data. Sessions with the same profile name share browser state."],
    proxy: [type: {:in, [:basic, :enhanced, :auto]}, doc: "Specifies the type of proxy to use.\n\n - **basic**: Proxies for scraping sites with none to basic anti-bot solutions. Fast and usually works.\n - **enhanced**: Enhanced proxies for scraping sites with advanced anti-bot solutions. Slower, but more reliable on certain sites. Costs up to 5 credits per request.\n - **auto**: Firecrawl will automatically retry scraping with enhanced proxies if the basic proxy fails. If the retry with enhanced is successful, 5 credits will be billed for the scrape. If the first attempt with basic is successful, only the regular cost will be billed."],
    remove_base64_images: [type: :boolean, doc: "Removes all base 64 images from the markdown output, which may be overwhelmingly long. This does not affect html or rawHtml formats. The image's alt text remains in the output, but the URL is replaced with a placeholder."],
    skip_tls_verification: [type: :boolean, doc: "Skip TLS certificate verification when making requests."],
    store_in_cache: [type: :boolean, doc: "If true, the page will be stored in the Firecrawl index and cache. Setting this to false is useful if your scraping activity may have data protection concerns. Using some parameters associated with sensitive scraping (e.g. actions, headers) will force this parameter to be false."],
    lockdown: [type: :boolean, doc: "If true, only previously cached results are served and no outbound request is ever made. If nothing is cached for the URL, a 404 with error code SCRAPE_LOCKDOWN_CACHE_MISS is returned. Other options are accepted but ignored in lockdown mode."],
    timeout: [type: :integer, doc: "Timeout in milliseconds for the request. Minimum is 1000 (1 second). Default is 60000 (60 seconds). Maximum is 300000 (300 seconds)."],
    wait_for: [type: :integer, doc: "Specify a delay in milliseconds before fetching the content, allowing the page sufficient time to load. This waiting time is in addition to Firecrawl's smart wait feature."],
    zero_data_retention: [type: :boolean, doc: "If true, this will enable zero data retention for this scrape. To enable this feature, please contact help@firecrawl.dev"]
  ])

  @scrape_and_extract_from_url_key_mapping %{url: "url", actions: "actions", block_ads: "blockAds", exclude_tags: "excludeTags", formats: "formats", headers: "headers", include_tags: "includeTags", location: "location", max_age: "maxAge", min_age: "minAge", mobile: "mobile", only_main_content: "onlyMainContent", parsers: "parsers", profile: "profile", proxy: "proxy", remove_base64_images: "removeBase64Images", skip_tls_verification: "skipTlsVerification", store_in_cache: "storeInCache", lockdown: "lockdown", timeout: "timeout", wait_for: "waitFor", zero_data_retention: "zeroDataRetention"}

  @doc """
  Scrape a single URL and optionally extract information using an LLM

  `POST /scrape`

  Tag: Scraping

  ## Parameters

  Validated by `NimbleOptions`. Pass params as a keyword list with snake_case keys.
  See `@scrape_and_extract_from_url_schema` for the full schema.

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec scrape_and_extract_from_url(keyword(), keyword()) :: response()
  def scrape_and_extract_from_url(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @scrape_and_extract_from_url_schema) do
      Req.post(client(opts), url: "/scrape", json: to_body(params, @scrape_and_extract_from_url_key_mapping))
    end
  end


  @doc """
  Bang variant of `scrape_and_extract_from_url`. Raises on error.
  """
  @spec scrape_and_extract_from_url!(keyword(), keyword()) :: Req.Response.t()
  def scrape_and_extract_from_url!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @scrape_and_extract_from_url_schema)
    Req.post!(client(opts), url: "/scrape", json: to_body(params, @scrape_and_extract_from_url_key_mapping))
  end


  @scrape_and_extract_from_urls_schema NimbleOptions.new!([
    ignore_invalid_urls: [type: :boolean, doc: "If invalid URLs are specified in the urls array, they will be ignored. Instead of them failing the entire request, a batch scrape using the remaining valid URLs will be created, and the invalid URLs will be returned in the invalidURLs field of the response."],
    max_concurrency: [type: :integer, doc: "Maximum number of concurrent scrapes. This parameter allows you to set a concurrency limit for this batch scrape. If not specified, the batch scrape adheres to your team's concurrency limit."],
    urls: [type: {:list, :string}, required: true],
    webhook: [type: :keyword_list, doc: "A webhook specification object."],
    actions: [type: {:list, :any}, doc: "Actions to perform on the page before grabbing the content"],
    block_ads: [type: :boolean, doc: "Enables ad-blocking and cookie popup blocking."],
    exclude_tags: [type: {:list, :string}, doc: "Tags to exclude from the output."],
    formats: [type: {:list, :any}, doc: "Output formats to include in the response. You can specify one or more formats, either as strings (e.g., `'markdown'`) or as objects with additional options (e.g., `{ type: 'json', schema: {...} }`, `{ type: 'question', question: '...' }`, `{ type: 'highlights', query: '...' }`). The legacy `{ type: 'query', prompt: '...', mode: 'freeform' | 'directQuote' }` format is deprecated."],
    headers: [type: :any, doc: "Headers to send with the request. Can be used to send cookies, user-agent, etc."],
    include_tags: [type: {:list, :string}, doc: "Tags to include in the output."],
    location: [type: :keyword_list, doc: "Location settings for the request. When specified, this will use an appropriate proxy if available and emulate the corresponding language and timezone settings. Defaults to 'US' if not specified."],
    max_age: [type: :integer, doc: "Returns a cached version of the page if it is younger than this age in milliseconds. If a cached version of the page is older than this value, the page will be scraped. If you do not need extremely fresh data, enabling this can speed up your scrapes by 500%. Defaults to 2 days."],
    min_age: [type: :integer, doc: "When set, the request only checks the cache and never triggers a fresh scrape. The value is in milliseconds and specifies the minimum age the cached data must be. If matching cached data exists, it is returned instantly. If no cached data is found, a 404 with error code SCRAPE_NO_CACHED_DATA is returned. Set to 1 to accept any cached data regardless of age."],
    mobile: [type: :boolean, doc: "Set to true if you want to emulate scraping from a mobile device. Useful for testing responsive pages and taking mobile screenshots."],
    only_main_content: [type: :boolean, doc: "Only return the main content of the page excluding headers, navs, footers, etc."],
    parsers: [type: {:list, :any}, doc: "Controls how files are processed during scraping. When \"pdf\" is included (default), the PDF content is extracted and converted to markdown format, with billing based on the number of pages (1 credit per page). When an empty array is passed, the PDF file is returned in base64 encoding with a flat rate of 1 credit for the entire PDF."],
    profile: [type: :keyword_list, doc: "Enable persistent browser storage across scrape and interact sessions. Pass a profile when scraping to preserve cookies, localStorage, and session data. Sessions with the same profile name share browser state."],
    proxy: [type: {:in, [:basic, :enhanced, :auto]}, doc: "Specifies the type of proxy to use.\n\n - **basic**: Proxies for scraping sites with none to basic anti-bot solutions. Fast and usually works.\n - **enhanced**: Enhanced proxies for scraping sites with advanced anti-bot solutions. Slower, but more reliable on certain sites. Costs up to 5 credits per request.\n - **auto**: Firecrawl will automatically retry scraping with enhanced proxies if the basic proxy fails. If the retry with enhanced is successful, 5 credits will be billed for the scrape. If the first attempt with basic is successful, only the regular cost will be billed."],
    remove_base64_images: [type: :boolean, doc: "Removes all base 64 images from the markdown output, which may be overwhelmingly long. This does not affect html or rawHtml formats. The image's alt text remains in the output, but the URL is replaced with a placeholder."],
    skip_tls_verification: [type: :boolean, doc: "Skip TLS certificate verification when making requests."],
    store_in_cache: [type: :boolean, doc: "If true, the page will be stored in the Firecrawl index and cache. Setting this to false is useful if your scraping activity may have data protection concerns. Using some parameters associated with sensitive scraping (e.g. actions, headers) will force this parameter to be false."],
    lockdown: [type: :boolean, doc: "If true, only previously cached results are served and no outbound request is ever made. If nothing is cached for the URL, a 404 with error code SCRAPE_LOCKDOWN_CACHE_MISS is returned. Other options are accepted but ignored in lockdown mode."],
    timeout: [type: :integer, doc: "Timeout in milliseconds for the request. Minimum is 1000 (1 second). Default is 60000 (60 seconds). Maximum is 300000 (300 seconds)."],
    wait_for: [type: :integer, doc: "Specify a delay in milliseconds before fetching the content, allowing the page sufficient time to load. This waiting time is in addition to Firecrawl's smart wait feature."],
    zero_data_retention: [type: :boolean, doc: "If true, this will enable zero data retention for this batch scrape. To enable this feature, please contact help@firecrawl.dev"]
  ])

  @scrape_and_extract_from_urls_key_mapping %{ignore_invalid_urls: "ignoreInvalidURLs", max_concurrency: "maxConcurrency", urls: "urls", webhook: "webhook", actions: "actions", block_ads: "blockAds", exclude_tags: "excludeTags", formats: "formats", headers: "headers", include_tags: "includeTags", location: "location", max_age: "maxAge", min_age: "minAge", mobile: "mobile", only_main_content: "onlyMainContent", parsers: "parsers", profile: "profile", proxy: "proxy", remove_base64_images: "removeBase64Images", skip_tls_verification: "skipTlsVerification", store_in_cache: "storeInCache", lockdown: "lockdown", timeout: "timeout", wait_for: "waitFor", zero_data_retention: "zeroDataRetention"}

  @doc """
  Scrape multiple URLs and optionally extract information using an LLM

  `POST /batch/scrape`

  Tag: Scraping

  ## Parameters

  Validated by `NimbleOptions`. Pass params as a keyword list with snake_case keys.
  See `@scrape_and_extract_from_urls_schema` for the full schema.

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec scrape_and_extract_from_urls(keyword(), keyword()) :: response()
  def scrape_and_extract_from_urls(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @scrape_and_extract_from_urls_schema) do
      Req.post(client(opts), url: "/batch/scrape", json: to_body(params, @scrape_and_extract_from_urls_key_mapping))
    end
  end


  @doc """
  Bang variant of `scrape_and_extract_from_urls`. Raises on error.
  """
  @spec scrape_and_extract_from_urls!(keyword(), keyword()) :: Req.Response.t()
  def scrape_and_extract_from_urls!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @scrape_and_extract_from_urls_schema)
    Req.post!(client(opts), url: "/batch/scrape", json: to_body(params, @scrape_and_extract_from_urls_key_mapping))
  end


  @search_and_scrape_schema NimbleOptions.new!([
    categories: [type: {:list, :any}, doc: "Categories to filter results by. Defaults to [], which means results will not be filtered by any categories."],
    country: [type: :string, doc: "ISO country code for geo-targeting search results (e.g. `US`). For best results, set both this and the `location` parameter."],
    enterprise: [type: {:list, :string}, doc: "Enterprise search options for Zero Data Retention (ZDR). Use `[\"zdr\"]` for end-to-end ZDR (10 credits / 10 results) or `[\"anon\"]` for anonymized ZDR (2 credits / 10 results). Must be enabled for your team."],
    exclude_domains: [type: {:list, :string}, doc: "Domains to exclude from search results."],
    ignore_invalid_urls: [type: :boolean, doc: "Excludes URLs from the search results that are invalid for other Firecrawl endpoints. This helps reduce errors if you are piping data from search into other Firecrawl API endpoints."],
    include_domains: [type: {:list, :string}, doc: "Domains to include in search results."],
    limit: [type: :integer, doc: "Maximum number of results to return"],
    location: [type: :string, doc: "Location parameter for search results (e.g. `San Francisco,California,United States`). For best results, set both this and the `country` parameter."],
    query: [type: :string, required: true, doc: "The search query"],
    scrape_options: [type: :keyword_list, doc: "Options for scraping search results"],
    sources: [type: {:list, :any}, doc: "Sources to search. Will determine the arrays available in the response. Defaults to ['web']."],
    tbs: [type: :string, doc: "Time-based search parameter. Supports predefined time ranges (`qdr:h`, `qdr:d`, `qdr:w`, `qdr:m`, `qdr:y`), custom date ranges (`cdr:1,cd_min:MM/DD/YYYY,cd_max:MM/DD/YYYY`), and sort by date (`sbd:1`). Values can be combined, e.g. `sbd:1,qdr:w`."],
    timeout: [type: :integer, doc: "Timeout in milliseconds"]
  ])

  @search_and_scrape_key_mapping %{categories: "categories", country: "country", enterprise: "enterprise", exclude_domains: "excludeDomains", ignore_invalid_urls: "ignoreInvalidURLs", include_domains: "includeDomains", limit: "limit", location: "location", query: "query", scrape_options: "scrapeOptions", sources: "sources", tbs: "tbs", timeout: "timeout"}

  @doc """
  Search and optionally scrape search results

  `POST /search`

  Tag: Search

  ## Parameters

  Validated by `NimbleOptions`. Pass params as a keyword list with snake_case keys.
  See `@search_and_scrape_schema` for the full schema.

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec search_and_scrape(keyword(), keyword()) :: response()
  def search_and_scrape(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @search_and_scrape_schema) do
      Req.post(client(opts), url: "/search", json: to_body(params, @search_and_scrape_key_mapping))
    end
  end


  @doc """
  Bang variant of `search_and_scrape`. Raises on error.
  """
  @spec search_and_scrape!(keyword(), keyword()) :: Req.Response.t()
  def search_and_scrape!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @search_and_scrape_schema)
    Req.post!(client(opts), url: "/search", json: to_body(params, @search_and_scrape_key_mapping))
  end


  @start_agent_schema NimbleOptions.new!([
    max_credits: [type: {:or, [:integer, :float]}, doc: "Maximum credits to spend on this agent task. Defaults to 2500 if not set. Values above 2,500 are always billed as paid requests."],
    model: [type: {:or, [{:in, [:"spark-1-mini", :"spark-1-pro"]}, :string]}, doc: "The model to use for the agent task. spark-1-mini (default) is 60% cheaper, spark-1-pro offers higher accuracy for complex tasks"],
    prompt: [type: :string, required: true, doc: "The prompt describing what data to extract"],
    schema: [type: :any, doc: "Optional JSON schema to structure the extracted data"],
    strict_constrain_to_urls: [type: :boolean, doc: "If true, agent will only visit URLs provided in the urls array"],
    urls: [type: {:list, :string}, doc: "Optional list of URLs to constrain the agent to"]
  ])

  @start_agent_key_mapping %{max_credits: "maxCredits", model: "model", prompt: "prompt", schema: "schema", strict_constrain_to_urls: "strictConstrainToURLs", urls: "urls"}

  @doc """
  Start an agent task for agentic data extraction

  `POST /agent`

  Tag: Agent

  ## Parameters

  Validated by `NimbleOptions`. Pass params as a keyword list with snake_case keys.
  See `@start_agent_schema` for the full schema.

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec start_agent(keyword(), keyword()) :: response()
  def start_agent(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @start_agent_schema) do
      Req.post(client(opts), url: "/agent", json: to_body(params, @start_agent_key_mapping))
    end
  end


  @doc """
  Bang variant of `start_agent`. Raises on error.
  """
  @spec start_agent!(keyword(), keyword()) :: Req.Response.t()
  def start_agent!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @start_agent_schema)
    Req.post!(client(opts), url: "/agent", json: to_body(params, @start_agent_key_mapping))
  end


  @doc """
  Stop the interactive browser session associated with a scrape job

  `DELETE /scrape/{jobId}/interact`

  Tag: Scraping

  ## Path Parameters

    * `job_id` - Path parameter `jobId`

  ## Returns

    * `{:ok, %Req.Response{}}` on success
    * `{:error, exception}` on HTTP or validation failure
  """
  @spec stop_interactive_scrape_browser_session(String.t(), keyword()) :: response()
  def stop_interactive_scrape_browser_session(job_id, opts \\ []) do
    Req.delete(client(opts), url: "/scrape/#{job_id}/interact")
  end


  @doc """
  Bang variant of `stop_interactive_scrape_browser_session`. Raises on error.
  """
  @spec stop_interactive_scrape_browser_session!(String.t(), keyword()) :: Req.Response.t()
  def stop_interactive_scrape_browser_session!(job_id, opts \\ []) do
    Req.delete!(client(opts), url: "/scrape/#{job_id}/interact")
  end

  @monitor_key_mapping %{
    name: "name",
    schedule: "schedule",
    targets: "targets",
    webhook: "webhook",
    notification: "notification",
    retention_days: "retentionDays",
    status: "status"
  }

  @monitor_list_key_mapping %{limit: "limit", offset: "offset"}
  @monitor_check_list_key_mapping %{limit: "limit", offset: "offset", status: "status"}
  @monitor_check_key_mapping %{limit: "limit", skip: "skip", status: "status"}

  @monitor_schema NimbleOptions.new!([
    name: [type: :string],
    schedule: [type: :any],
    targets: [type: {:list, :any}],
    webhook: [type: :any],
    notification: [type: :any],
    retention_days: [type: :integer],
    status: [type: :any]
  ])

  @monitor_list_schema NimbleOptions.new!([
    limit: [type: :integer],
    offset: [type: :integer]
  ])

  @monitor_check_list_schema NimbleOptions.new!([
    limit: [type: :integer],
    offset: [type: :integer],
    status: [type: :any]
  ])

  @monitor_check_schema NimbleOptions.new!([
    limit: [type: :integer],
    skip: [type: :integer],
    status: [type: :any]
  ])

  @doc """
  Create a scheduled monitor.

  `POST /monitor`
  """
  @spec create_monitor(keyword(), keyword()) :: response()
  def create_monitor(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @monitor_schema) do
      Req.post(client(opts), url: "/monitor", json: to_body(params, @monitor_key_mapping))
    end
  end

  @doc """
  Bang variant of `create_monitor`. Raises on error.
  """
  @spec create_monitor!(keyword(), keyword()) :: Req.Response.t()
  def create_monitor!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @monitor_schema)
    Req.post!(client(opts), url: "/monitor", json: to_body(params, @monitor_key_mapping))
  end

  @doc """
  List monitors for the authenticated team.

  `GET /monitor`
  """
  @spec list_monitors(keyword(), keyword()) :: response()
  def list_monitors(params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @monitor_list_schema) do
      Req.get(client(opts), url: "/monitor", params: to_query(params, @monitor_list_key_mapping))
    end
  end

  @doc """
  Bang variant of `list_monitors`. Raises on error.
  """
  @spec list_monitors!(keyword(), keyword()) :: Req.Response.t()
  def list_monitors!(params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @monitor_list_schema)
    Req.get!(client(opts), url: "/monitor", params: to_query(params, @monitor_list_key_mapping))
  end

  @doc """
  Get a monitor by ID.

  `GET /monitor/{monitorId}`
  """
  @spec get_monitor(String.t(), keyword()) :: response()
  def get_monitor(monitor_id, opts \\ []) do
    Req.get(client(opts), url: "/monitor/#{monitor_id}")
  end

  @doc """
  Bang variant of `get_monitor`. Raises on error.
  """
  @spec get_monitor!(String.t(), keyword()) :: Req.Response.t()
  def get_monitor!(monitor_id, opts \\ []) do
    Req.get!(client(opts), url: "/monitor/#{monitor_id}")
  end

  @doc """
  Update a monitor.

  `PATCH /monitor/{monitorId}`
  """
  @spec update_monitor(String.t(), keyword(), keyword()) :: response()
  def update_monitor(monitor_id, params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @monitor_schema) do
      Req.patch(client(opts), url: "/monitor/#{monitor_id}", json: to_body(params, @monitor_key_mapping))
    end
  end

  @doc """
  Bang variant of `update_monitor`. Raises on error.
  """
  @spec update_monitor!(String.t(), keyword(), keyword()) :: Req.Response.t()
  def update_monitor!(monitor_id, params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @monitor_schema)
    Req.patch!(client(opts), url: "/monitor/#{monitor_id}", json: to_body(params, @monitor_key_mapping))
  end

  @doc """
  Delete a monitor.

  `DELETE /monitor/{monitorId}`
  """
  @spec delete_monitor(String.t(), keyword()) :: response()
  def delete_monitor(monitor_id, opts \\ []) do
    Req.delete(client(opts), url: "/monitor/#{monitor_id}")
  end

  @doc """
  Bang variant of `delete_monitor`. Raises on error.
  """
  @spec delete_monitor!(String.t(), keyword()) :: Req.Response.t()
  def delete_monitor!(monitor_id, opts \\ []) do
    Req.delete!(client(opts), url: "/monitor/#{monitor_id}")
  end

  @doc """
  Trigger a manual monitor check.

  `POST /monitor/{monitorId}/run`
  """
  @spec run_monitor(String.t(), keyword()) :: response()
  def run_monitor(monitor_id, opts \\ []) do
    Req.post(client(opts), url: "/monitor/#{monitor_id}/run", json: %{})
  end

  @doc """
  Bang variant of `run_monitor`. Raises on error.
  """
  @spec run_monitor!(String.t(), keyword()) :: Req.Response.t()
  def run_monitor!(monitor_id, opts \\ []) do
    Req.post!(client(opts), url: "/monitor/#{monitor_id}/run", json: %{})
  end

  @doc """
  List checks for a monitor.

  `GET /monitor/{monitorId}/checks`
  """
  @spec list_monitor_checks(String.t(), keyword(), keyword()) :: response()
  def list_monitor_checks(monitor_id, params \\ [], opts \\ []) do
    with {:ok, params} <- NimbleOptions.validate(params, @monitor_check_list_schema) do
      Req.get(client(opts),
        url: "/monitor/#{monitor_id}/checks",
        params: to_query(params, @monitor_check_list_key_mapping)
      )
    end
  end

  @doc """
  Bang variant of `list_monitor_checks`. Raises on error.
  """
  @spec list_monitor_checks!(String.t(), keyword(), keyword()) :: Req.Response.t()
  def list_monitor_checks!(monitor_id, params \\ [], opts \\ []) do
    params = NimbleOptions.validate!(params, @monitor_check_list_schema)
    Req.get!(client(opts),
      url: "/monitor/#{monitor_id}/checks",
      params: to_query(params, @monitor_check_list_key_mapping)
    )
  end

  @doc """
  Get a monitor check with paginated page results and inline diffs.

  `GET /monitor/{monitorId}/checks/{checkId}`
  """
  @spec get_monitor_check(String.t(), String.t(), keyword(), keyword()) :: response()
  def get_monitor_check(monitor_id, check_id, params \\ [], opts \\ []) do
    {auto_paginate, opts} = Keyword.pop(opts, :auto_paginate, true)

    with {:ok, params} <- NimbleOptions.validate(params, @monitor_check_schema) do
      case Req.get(client(opts),
             url: "/monitor/#{monitor_id}/checks/#{check_id}",
             params: to_query(params, @monitor_check_key_mapping)
           ) do
        {:ok, response} when auto_paginate -> {:ok, paginate_monitor_check_response(response, opts)}
        result -> result
      end
    end
  end

  @doc """
  Bang variant of `get_monitor_check`. Raises on error.
  """
  @spec get_monitor_check!(String.t(), String.t(), keyword(), keyword()) :: Req.Response.t()
  def get_monitor_check!(monitor_id, check_id, params \\ [], opts \\ []) do
    {auto_paginate, opts} = Keyword.pop(opts, :auto_paginate, true)
    params = NimbleOptions.validate!(params, @monitor_check_schema)
    response = Req.get!(client(opts),
      url: "/monitor/#{monitor_id}/checks/#{check_id}",
      params: to_query(params, @monitor_check_key_mapping)
    )

    if auto_paginate, do: paginate_monitor_check_response!(response, opts), else: response
  end

  defp paginate_monitor_check_response(%Req.Response{body: body} = response, opts) when is_map(body) do
    data = Map.get(body, "data") || %{}
    pages = Map.get(data, "pages") || []
    next = Map.get(body, "next") || Map.get(data, "next")
    pages = fetch_monitor_check_pages(next, pages, opts)
    data = data |> Map.put("pages", pages) |> Map.put("next", nil)
    %{response | body: body |> Map.put("data", data) |> Map.put("next", nil)}
  end

  defp paginate_monitor_check_response(response, _opts), do: response

  defp paginate_monitor_check_response!(%Req.Response{body: body} = response, opts) when is_map(body) do
    data = Map.get(body, "data") || %{}
    pages = Map.get(data, "pages") || []
    next = Map.get(body, "next") || Map.get(data, "next")
    pages = fetch_monitor_check_pages!(next, pages, opts)
    data = data |> Map.put("pages", pages) |> Map.put("next", nil)
    %{response | body: body |> Map.put("data", data) |> Map.put("next", nil)}
  end

  defp paginate_monitor_check_response!(response, _opts), do: response

  defp fetch_monitor_check_pages(nil, pages, _opts), do: pages
  defp fetch_monitor_check_pages("", pages, _opts), do: pages

  defp fetch_monitor_check_pages(next, pages, opts) do
    case Req.get(client(opts), url: next) do
      {:ok, %Req.Response{body: body}} when is_map(body) ->
        data = Map.get(body, "data") || %{}
        next_pages = Map.get(data, "pages") || []
        next_url = Map.get(body, "next") || Map.get(data, "next")
        fetch_monitor_check_pages(next_url, pages ++ next_pages, opts)

      _ ->
        pages
    end
  end

  defp fetch_monitor_check_pages!(nil, pages, _opts), do: pages
  defp fetch_monitor_check_pages!("", pages, _opts), do: pages

  defp fetch_monitor_check_pages!(next, pages, opts) do
    response = Req.get!(client(opts), url: next)
    body = response.body
    data = if is_map(body), do: Map.get(body, "data") || %{}, else: %{}
    next_pages = Map.get(data, "pages") || []
    next_url = if is_map(body), do: Map.get(body, "next") || Map.get(data, "next"), else: nil
    fetch_monitor_check_pages!(next_url, pages ++ next_pages, opts)
  end

end
