defmodule Towerops.Security.CloudflareClient do
  @moduledoc """
  Client for interacting with Cloudflare's API to manage IP Access Rules (WAF).

  Used to push permanent IP bans to Cloudflare's edge network for
  pre-application blocking of brute force scanners.
  """

  require Logger

  @base_url "https://api.cloudflare.com/client/v4"

  @doc """
  Blocks an IP address at the Cloudflare edge using WAF IP Access Rules.

  Returns `{:ok, response}` on success.
  Returns `{:error, reason}` on failure.
  """
  def block_ip(ip_address) do
    with {:ok, zone_id} <- get_config(:cloudflare_zone_id),
         {:ok, api_token} <- get_config(:cloudflare_api_token) do
      body = %{
        mode: "block",
        configuration: %{
          target: "ip",
          value: ip_address
        },
        notes: "Towerops brute force protection - permanent ban"
      }

      url = "#{@base_url}/zones/#{zone_id}/firewall/access_rules/rules"

      case Req.post(url,
             json: body,
             headers: [
               {"authorization", "Bearer #{api_token}"},
               {"content-type", "application/json"}
             ]
           ) do
        {:ok, %{status: status, body: response}} when status in 200..299 ->
          Logger.info("Successfully blocked IP at Cloudflare: #{ip_address}")
          {:ok, response}

        {:ok, %{status: status, body: body}} ->
          Logger.error("Cloudflare API error (#{status}): #{inspect(body)}")
          {:error, "API returned status #{status}"}

        {:error, reason} ->
          Logger.error("Failed to call Cloudflare API: #{inspect(reason)}")
          {:error, reason}
      end
    else
      {:error, :missing_config} ->
        Logger.warning("Cloudflare credentials not configured, skipping edge block for #{ip_address}")
        {:error, :not_configured}
    end
  end

  @doc """
  Unblocks an IP address by finding and deleting the corresponding WAF rule.

  Note: This requires listing all rules to find the matching one, which may be slow.
  """
  def unblock_ip(ip_address) do
    with {:ok, zone_id} <- get_config(:cloudflare_zone_id),
         {:ok, api_token} <- get_config(:cloudflare_api_token),
         {:ok, rule_id} <- find_rule_by_ip(zone_id, api_token, ip_address) do
      url = "#{@base_url}/zones/#{zone_id}/firewall/access_rules/rules/#{rule_id}"

      case Req.delete(url,
             headers: [
               {"authorization", "Bearer #{api_token}"}
             ]
           ) do
        {:ok, %{status: status}} when status in 200..299 ->
          Logger.info("Successfully unblocked IP at Cloudflare: #{ip_address}")
          {:ok, :deleted}

        {:ok, %{status: status, body: body}} ->
          Logger.error("Cloudflare API error (#{status}): #{inspect(body)}")
          {:error, "API returned status #{status}"}

        {:error, reason} ->
          Logger.error("Failed to call Cloudflare API: #{inspect(reason)}")
          {:error, reason}
      end
    else
      {:error, :not_found} ->
        Logger.warning("No Cloudflare rule found for IP: #{ip_address}")
        {:error, :not_found}

      {:error, :missing_config} ->
        {:error, :not_configured}

      error ->
        error
    end
  end

  defp find_rule_by_ip(zone_id, api_token, ip_address) do
    url = "#{@base_url}/zones/#{zone_id}/firewall/access_rules/rules"

    case Req.get(url,
           headers: [
             {"authorization", "Bearer #{api_token}"}
           ],
           params: [
             {"configuration.value", ip_address},
             {"mode", "block"}
           ]
         ) do
      {:ok, %{status: 200, body: %{"result" => [rule | _]}}} ->
        {:ok, rule["id"]}

      {:ok, %{status: 200, body: %{"result" => []}}} ->
        {:error, :not_found}

      {:ok, %{status: status, body: body}} ->
        Logger.error("Cloudflare API error (#{status}): #{inspect(body)}")
        {:error, "API returned status #{status}"}

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

  defp get_config(key) do
    case Application.get_env(:towerops, key) do
      nil -> {:error, :missing_config}
      value -> {:ok, value}
    end
  end
end
