defmodule Towerops.Gaiia.Client do
  @moduledoc """
  GraphQL client for the Gaiia API.

  Uses Req with built-in test support via `Req.Test`. Handles Relay-style
  cursor pagination for all list endpoints.
  """

  require Logger

  @default_endpoint "https://api.gaiia.com/api/v1"

  @accounts_query """
  query ListAccounts($first: Int!, $after: String) {
    accounts(first: $first, after: $after) {
      edges {
        node {
          id
          readableId
          name
          status {
            id
            name
            type
          }
          type {
            id
            name
          }
          physicalAddress {
            line1
            locality
            region
            postalCode
            country
            latitude
            longitude
          }
          billingSubscriptions(first: 100) {
            edges {
              node {
                id
                status {
                  id
                  name
                }
              }
            }
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
  """

  @network_sites_query """
  query ListNetworkSites($first: Int!, $after: String) {
    networkSites(first: $first, after: $after) {
      edges {
        node {
          id
          name
          address {
            line1
            locality
            region
            postalCode
            country
            latitude
            longitude
          }
          ipBlocks(first: 100) {
            edges {
              node {
                subnet
              }
            }
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
  """

  @inventory_items_query """
  query ListInventoryItems($first: Int!, $after: String) {
    inventoryItems(first: $first, after: $after) {
      edges {
        node {
          id
          status {
            id
            name
          }
          ipAddressV4
          model {
            name
            manufacturer {
              name
            }
            category {
              name
            }
          }
          assignation {
            assigneeType
            assignee {
              ... on Account { id }
              ... on NetworkSite { id }
            }
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
  """

  @billing_subscriptions_query """
  query ListBillingSubscriptions($first: Int!, $after: String) {
    billingSubscriptions(first: $first, after: $after) {
      edges {
        node {
          id
          status {
            id
            name
          }
          productVersion {
            product {
              name
            }
            price
            currency
          }
          entity {
            ... on Account { id }
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
  """

  # --- Mutations ---

  @create_ticket_mutation """
  mutation CreateExternalTicket($subject: String!, $description: String!, $entityId: ID, $entityType: String) {
    createExternalTicket(input: {
      subject: $subject,
      description: $description,
      entityId: $entityId,
      entityType: $entityType
    }) {
      id
      subject
    }
  }
  """

  @create_note_mutation """
  mutation CreateNote($entityId: ID!, $entityType: String!, $body: String!) {
    createNote(input: {
      entityId: $entityId,
      entityType: $entityType,
      body: $body
    }) {
      id
    }
  }
  """

  @update_inventory_item_mutation """
  mutation UpdateInventoryItem($id: ID!, $input: UpdateInventoryItemInput!) {
    updateInventoryItem(id: $id, input: $input) {
      id
      ipAddressV4
    }
  }
  """

  @doc "Create an external ticket in Gaiia."
  def create_ticket(api_key, attrs, opts \\ []) do
    query(api_key, @create_ticket_mutation, attrs, opts)
  end

  @doc "Create a note on a Gaiia entity (account, inventory item, etc)."
  def create_note(api_key, entity_type, entity_id, body, opts \\ []) do
    variables = %{
      "entityId" => entity_id,
      "entityType" => entity_type,
      "body" => body
    }

    query(api_key, @create_note_mutation, variables, opts)
  end

  @doc "Update an inventory item in Gaiia."
  def update_inventory_item(api_key, item_id, input, opts \\ []) do
    query(api_key, @update_inventory_item_mutation, %{"id" => item_id, "input" => input}, opts)
  end

  # --- Queries ---

  @doc "Test the connection to Gaiia by fetching a single account."
  def test_connection(api_key, opts \\ []) do
    case query(api_key, "{ accounts(first: 1) { edges { node { id } } } }", %{}, opts) do
      {:ok, _data} -> {:ok, %{}}
      {:error, reason} -> {:error, reason}
    end
  end

  @doc "List all accounts with automatic pagination."
  def list_accounts(api_key, opts \\ []) do
    paginate(api_key, @accounts_query, "accounts", opts)
  end

  @doc "List all network sites with automatic pagination."
  def list_network_sites(api_key, opts \\ []) do
    paginate(api_key, @network_sites_query, "networkSites", opts)
  end

  @doc "List all inventory items with automatic pagination."
  def list_inventory_items(api_key, opts \\ []) do
    paginate(api_key, @inventory_items_query, "inventoryItems", opts)
  end

  @doc "List all billing subscriptions with automatic pagination."
  def list_billing_subscriptions(api_key, opts \\ []) do
    paginate(api_key, @billing_subscriptions_query, "billingSubscriptions", opts)
  end

  @doc "Execute a raw GraphQL query against the Gaiia API."
  def query(api_key, query_string, variables \\ %{}, opts \\ []) do
    case request(api_key, query_string, variables, opts) do
      {:ok, %{status: status, body: %{"data" => data}}} when status in 200..299 ->
        {:ok, data}

      {:ok, %{status: status, body: %{"errors" => errors}}} when status in 200..299 ->
        {:error, {:graphql_errors, errors}}

      {:ok, %{status: 400, body: %{"errors" => errors}}} ->
        {:error, {:graphql_errors, errors}}

      {:ok, %{status: 401}} ->
        {:error, :unauthorized}

      {:ok, %{status: 403}} ->
        {:error, :forbidden}

      {:ok, %{status: 429, headers: headers}} ->
        retry_after = get_retry_after(headers)
        {:error, {:rate_limited, retry_after}}

      {:ok, %{status: status, body: body}} ->
        Logger.warning("Gaiia API unexpected status #{status}: #{inspect(body)}")
        {:error, {:unexpected_status, status}}

      {:error, reason} ->
        Logger.error("Gaiia API connection error: #{inspect(reason)}")
        {:error, reason}
    end
  end

  defp paginate(api_key, query_string, root_key, opts, cursor \\ nil, acc \\ []) do
    variables = %{"first" => 100, "after" => cursor}

    case query(api_key, query_string, variables, opts) do
      {:ok, data} ->
        root = data[root_key]
        nodes = Enum.map(root["edges"], & &1["node"])
        all_nodes = acc ++ nodes

        page_info = root["pageInfo"]

        if page_info["hasNextPage"] do
          paginate(api_key, query_string, root_key, opts, page_info["endCursor"], all_nodes)
        else
          {:ok, all_nodes}
        end

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

  defp request(api_key, query_string, variables, opts) do
    endpoint = Keyword.get(opts, :endpoint, @default_endpoint)

    req_opts = [
      method: :post,
      url: endpoint,
      headers: [
        {"x-gaiia-api-key", api_key},
        {"content-type", "application/json"}
      ],
      json: %{"query" => query_string, "variables" => variables}
    ]

    req_opts =
      if Application.get_env(:towerops, :env) == :test do
        Keyword.put(req_opts, :plug, {Req.Test, __MODULE__})
      else
        req_opts
      end

    Req.request(req_opts)
  rescue
    exception ->
      {:error, Exception.message(exception)}
  end

  defp get_retry_after(headers) when is_map(headers) do
    case Map.get(headers, "retry-after") do
      [value | _] -> parse_retry_after(value)
      _ -> 60
    end
  end

  defp get_retry_after(_headers), do: 60

  defp parse_retry_after(value) when is_binary(value) do
    case Integer.parse(value) do
      {seconds, _} -> seconds
      :error -> 60
    end
  end

  defp parse_retry_after(value) when is_integer(value), do: value
  defp parse_retry_after(_), do: 60
end
