defmodule Towerops.Gaiia.Sync do
  @moduledoc """
  Orchestrates syncing data from the Gaiia GraphQL API into the local database.

  Pulls accounts, network sites, inventory items, and billing subscriptions
  from Gaiia, upserts them into local cache tables, and updates integration
  sync status.
  """

  alias Towerops.Gaiia
  alias Towerops.Gaiia.Client
  alias Towerops.Integrations

  require Logger

  @doc """
  Main entry point: syncs all entity types for the given integration.

  Returns `{:ok, %{accounts: n, network_sites: n, inventory_items: n, subscriptions: n}}`
  or `{:error, reason}`.
  """
  def sync_organization(%Integrations.Integration{} = integration) do
    api_key = integration.credentials["api_key"]
    org_id = integration.organization_id
    opts = client_opts(integration)

    with {:ok, accounts} <- Client.list_accounts(api_key, opts),
         accounts_count = upsert_nodes(org_id, accounts, :account),
         {:ok, sites} <- Client.list_network_sites(api_key, opts),
         sites_count = upsert_nodes(org_id, sites, :network_site),
         {:ok, items} <- Client.list_inventory_items(api_key, opts),
         items_count = upsert_nodes(org_id, items, :inventory_item),
         {:ok, subs} <- Client.list_billing_subscriptions(api_key, opts) do
      subs_count = upsert_nodes(org_id, subs, :billing_subscription)
      Integrations.update_sync_status(integration, "success")

      {:ok,
       %{
         accounts: accounts_count,
         network_sites: sites_count,
         inventory_items: items_count,
         subscriptions: subs_count
       }}
    else
      {:error, reason} ->
        Integrations.update_sync_status(integration, "failed")
        {:error, reason}
    end
  end

  defp upsert_nodes(org_id, nodes, type) do
    Enum.reduce(nodes, 0, fn node, count ->
      case upsert_node(org_id, node, type) do
        {:ok, _} ->
          count + 1

        {:error, changeset} ->
          Logger.warning("Failed to upsert Gaiia #{type}: #{inspect(changeset.errors)}")
          count
      end
    end)
  end

  defp upsert_node(org_id, node, :account) do
    Gaiia.upsert_account(org_id, map_account(node))
  end

  defp upsert_node(org_id, node, :network_site) do
    Gaiia.upsert_network_site(org_id, map_network_site(node))
  end

  defp upsert_node(org_id, node, :inventory_item) do
    Gaiia.upsert_inventory_item(org_id, map_inventory_item(node))
  end

  defp upsert_node(org_id, node, :billing_subscription) do
    Gaiia.upsert_billing_subscription(org_id, map_billing_subscription(node))
  end

  defp map_account(node) do
    subs = get_in(node, ["billingSubscriptions", "edges"]) || []
    active_count = Enum.count(subs, &(get_in(&1, ["node", "status", "name"]) == "Active"))

    %{
      gaiia_id: node["id"],
      readable_id: node["readableId"],
      name: node["name"],
      status: get_in(node, ["status", "name"]),
      account_type: get_in(node, ["type", "name"]),
      address: map_address(node["physicalAddress"]),
      subscription_count: active_count,
      raw_data: node
    }
  end

  defp map_network_site(node) do
    ip_block_edges = get_in(node, ["ipBlocks", "edges"]) || []
    ip_blocks = Enum.map(ip_block_edges, & &1["node"]["subnet"])

    %{
      gaiia_id: node["id"],
      name: node["name"],
      address: map_address(node["address"]),
      ip_blocks: ip_blocks,
      raw_data: node
    }
  end

  defp map_inventory_item(node) do
    assignation = node["assignation"]

    {account_id, site_id} =
      case assignation do
        %{"assigneeType" => "Account", "assignee" => %{"id" => id}} -> {id, nil}
        %{"assigneeType" => "NetworkSite", "assignee" => %{"id" => id}} -> {nil, id}
        _ -> {nil, nil}
      end

    %{
      gaiia_id: node["id"],
      name: get_in(node, ["model", "name"]),
      status: get_in(node, ["status", "name"]),
      serial_number: nil,
      mac_address: nil,
      ip_address: node["ipAddressV4"],
      model_name: get_in(node, ["model", "name"]),
      manufacturer_name: get_in(node, ["model", "manufacturer", "name"]),
      category: get_in(node, ["model", "category", "name"]),
      assigned_account_gaiia_id: account_id,
      assigned_network_site_gaiia_id: site_id,
      raw_data: node
    }
  end

  defp map_billing_subscription(node) do
    price_cents = get_in(node, ["productVersion", "price"])

    mrr_amount =
      if is_integer(price_cents),
        do: Decimal.div(Decimal.new(price_cents), 100)

    %{
      gaiia_id: node["id"],
      account_gaiia_id: get_in(node, ["entity", "id"]),
      status: get_in(node, ["status", "name"]),
      product_name: get_in(node, ["productVersion", "product", "name"]),
      mrr_amount: mrr_amount,
      currency: get_in(node, ["productVersion", "currency"]),
      raw_data: node
    }
  end

  defp map_address(nil), do: nil

  defp map_address(addr) do
    %{
      "line1" => addr["line1"],
      "city" => addr["locality"],
      "state" => addr["region"],
      "zip" => addr["postalCode"],
      "country" => addr["country"],
      "latitude" => addr["latitude"],
      "longitude" => addr["longitude"]
    }
  end

  defp client_opts(integration) do
    case integration.credentials["endpoint"] do
      nil -> []
      endpoint -> [endpoint: endpoint]
    end
  end
end
