defmodule Plausible.Google.GA4.HTTP do
  @moduledoc """
  HTTP client implementation for Google Analytics 4 API.
  """

  use Plausible

  alias Plausible.HTTPClient

  require Logger

  @spec get_report(Plausible.Google.GA4.ReportRequest.t()) ::
          {:ok, {[map()], non_neg_integer()}} | {:error, any()}
  def get_report(%Plausible.Google.GA4.ReportRequest{} = report_request) do
    params = %{
      requests: [
        %{
          property: report_request.property,
          dateRanges: [
            %{
              startDate: report_request.date_range.first,
              endDate: report_request.date_range.last
            }
          ],
          dimensions: Enum.map(report_request.dimensions, &%{name: &1}),
          metrics: Enum.map(report_request.metrics, &build_metric/1),
          orderBys: [
            %{
              dimension: %{
                dimensionName: "date",
                orderType: "ALPHANUMERIC"
              },
              desc: true
            }
          ],
          dimensionFilter: report_request.dimension_filter,
          limit: report_request.limit,
          offset: report_request.offset
        }
      ]
    }

    url =
      "#{reporting_api_url()}/v1beta/#{report_request.property}:batchRunReports"

    response =
      HTTPClient.impl().post(
        url,
        [{"Authorization", "Bearer #{report_request.access_token}"}],
        params,
        receive_timeout: 80_000
      )

    with {:ok, %{body: body}} <- response,
         {:ok, report} <- parse_report_from_response(body),
         row_count <- Map.get(report, "rowCount", 0),
         {:ok, report} <- convert_to_maps(report) do
      {:ok, {report, row_count}}
    else
      {:error, %{reason: %{status: 429, body: body}} = error} ->
        log_ce_error("retrieving report for #{report_request.dataset}", error)

        Logger.debug(
          "[#{inspect(__MODULE__)}:#{report_request.property}] Request failed for #{report_request.dataset} due to exceeding rate limit."
        )

        Sentry.Context.set_extra_context(%{ga_response: %{body: body, status: 429}})

        {:error,
         {:rate_limit_exceeded, dataset: report_request.dataset, offset: report_request.offset}}

      {:error, %{reason: %{status: status, body: body}} = error} when status >= 500 ->
        log_ce_error("retrieving report for #{report_request.dataset}", error)

        Logger.warning(
          "[#{inspect(__MODULE__)}:#{report_request.property}] Request failed for #{report_request.dataset} with code #{status}: #{inspect(body)}"
        )

        Sentry.Context.set_extra_context(%{ga_response: %{body: body, status: status}})

        {:error, {:server_failed, dataset: report_request.dataset, offset: report_request.offset}}

      {:error, %{reason: %{status: status, body: body}} = error} ->
        log_ce_error("retrieving report for #{report_request.dataset}", error)

        Logger.warning(
          "[#{inspect(__MODULE__)}:#{report_request.property}] Request failed for #{report_request.dataset} with code #{status}: #{inspect(body)}"
        )

        Sentry.Context.set_extra_context(%{ga_response: %{body: body, status: status}})

        {:error, :request_failed}

      {:error, reason} ->
        log_ce_error("retrieving report for #{report_request.dataset}", reason)

        Logger.warning(
          "[#{inspect(__MODULE__)}:#{report_request.property}] Request failed for #{report_request.dataset}: #{inspect(reason)}"
        )

        Sentry.Context.set_extra_context(%{ga_response: %{body: inspect(reason), status: 0}})

        {:error, {:socket_failed, dataset: report_request.dataset, offset: report_request.offset}}
    end
  end

  defp build_metric(expression) do
    case String.split(expression, " = ") do
      [name, expression] ->
        %{
          name: name,
          expression: expression
        }

      [name] ->
        %{name: name}
    end
  end

  defp parse_report_from_response(%{"reports" => [report | _]}) do
    {:ok, report}
  end

  defp parse_report_from_response(body) do
    Sentry.Context.set_extra_context(%{google_analytics4_response: body})

    Logger.error(
      "Google Analytics 4: Failed to find report in response. Reason: #{inspect(body)}"
    )

    {:error, {:invalid_response, body}}
  end

  defp convert_to_maps(%{
         "rows" => rows,
         "dimensionHeaders" => dimension_headers,
         "metricHeaders" => metric_headers
       })
       when is_list(rows) do
    dimension_headers = Enum.map(dimension_headers, & &1["name"])
    metric_headers = Enum.map(metric_headers, & &1["name"])

    report =
      Enum.map(rows, fn %{"dimensionValues" => dimensions, "metricValues" => metrics} ->
        dimension_values = Enum.map(dimensions, & &1["value"])
        metric_values = Enum.map(metrics, & &1["value"])
        metrics = Enum.zip(metric_headers, metric_values)
        dimensions = Enum.zip(dimension_headers, dimension_values)
        %{metrics: Map.new(metrics), dimensions: Map.new(dimensions)}
      end)

    {:ok, report}
  end

  defp convert_to_maps(%{"dimensionHeaders" => _, "metricHeaders" => _}) do
    {:ok, []}
  end

  defp convert_to_maps(response) do
    Logger.error(
      "Google Analytics 4: Failed to read report in response. Reason: #{inspect(response)}"
    )

    Sentry.Context.set_extra_context(%{google_analytics4_response: response})
    {:error, {:invalid_response, response}}
  end

  def list_accounts_for_user(access_token) do
    url = "#{admin_api_url()}/v1beta/accountSummaries?pageSize=200"

    headers = [{"Authorization", "Bearer #{access_token}"}]

    case HTTPClient.impl().get(url, headers) do
      {:ok, %Finch.Response{body: body, status: 200}} ->
        {:ok, body}

      {:error, %HTTPClient.Non200Error{reason: %{status: 429}} = error} ->
        log_ce_error("listing accounts for user", error)
        {:error, :rate_limit_exceeded}

      {:error, %HTTPClient.Non200Error{} = error} when error.reason.status in [401, 403] ->
        log_ce_error("listing accounts for user", error)
        {:error, authentication_failed(error)}

      {:error, %{reason: :timeout} = error} ->
        log_ce_error("listing accounts for user", error)
        {:error, :timeout}

      {:error, error} ->
        log_ce_error("listing accounts for user", error)
        Sentry.capture_message("Error listing GA4 accounts for user", extra: %{error: error})
        {:error, :unknown}
    end
  end

  def get_property(access_token, property) do
    url = "#{admin_api_url()}/v1beta/#{property}"

    headers = [{"Authorization", "Bearer #{access_token}"}]

    case HTTPClient.impl().get(url, headers) do
      {:ok, %Finch.Response{body: body, status: 200}} ->
        {:ok, body}

      {:error, %HTTPClient.Non200Error{reason: %{status: 429}} = error} ->
        log_ce_error("retrieving property #{property}", error)
        {:error, :rate_limit_exceeded}

      {:error, %HTTPClient.Non200Error{} = error} when error.reason.status in [401, 403] ->
        log_ce_error("retrieving property #{property}", error)
        {:error, authentication_failed(error)}

      {:error, %HTTPClient.Non200Error{} = error} when error.reason.status in [404] ->
        log_ce_error("retrieving property #{property}", error)
        {:error, :not_found}

      {:error, %{reason: :timeout} = error} ->
        log_ce_error("retrieving property #{property}", error)
        {:error, :timeout}

      {:error, error} ->
        log_ce_error("retrieving property #{property}", error)

        Sentry.capture_message("Error retrieving GA4 property #{property}",
          extra: %{error: error}
        )

        {:error, :unknown}
    end
  end

  @earliest_valid_date "2015-08-14"

  def get_analytics_start_date(access_token, property) do
    get_analytics_boundary_date(access_token, property, :start)
  end

  def get_analytics_end_date(access_token, property) do
    get_analytics_boundary_date(access_token, property, :end)
  end

  defp get_analytics_boundary_date(access_token, property, edge) do
    descending? = edge == :end

    params = %{
      requests: [
        %{
          property: "#{property}",
          dateRanges: [
            %{startDate: @earliest_valid_date, endDate: Date.to_iso8601(Date.utc_today())}
          ],
          dimensions: [%{name: "date"}],
          metrics: [%{name: "screenPageViews"}],
          orderBys: [
            %{dimension: %{dimensionName: "date", orderType: "ALPHANUMERIC"}, desc: descending?}
          ],
          limit: 1
        }
      ]
    }

    url = "#{reporting_api_url()}/v1beta/#{property}:batchRunReports"
    headers = [{"Authorization", "Bearer #{access_token}"}]

    case HTTPClient.impl().post(url, headers, params) do
      {:ok, %Finch.Response{body: body, status: 200}} ->
        report = List.first(body["reports"])

        date =
          case report["rows"] do
            [%{"dimensionValues" => [%{"value" => date_str}]}] ->
              Plausible.Times.parse!(date_str, "%Y%m%d", :strftime) |> NaiveDateTime.to_date()

            _ ->
              nil
          end

        {:ok, date}

      {:error, %HTTPClient.Non200Error{reason: %{status: 429}} = error} ->
        log_ce_error("retrieving #{edge} date", error)
        {:error, :rate_limit_exceeded}

      {:error, %HTTPClient.Non200Error{} = error} when error.reason.status in [401, 403] ->
        log_ce_error("retrieving #{edge} date", error)
        {:error, authentication_failed(error)}

      {:error, %{reason: :timeout} = error} ->
        log_ce_error("retrieving #{edge} date", error)
        {:error, :timeout}

      {:error, error} ->
        log_ce_error("retrieving #{edge} date", error)

        Sentry.capture_message("Error retrieving GA4 #{edge} date",
          extra: %{error: error}
        )

        {:error, :unknown}
    end
  end

  defp reporting_api_url, do: "https://analyticsdata.googleapis.com"
  defp admin_api_url, do: "https://analyticsadmin.googleapis.com"

  @spec authentication_failed(HTTPClient.Non200Error.t()) ::
          {:authentication_failed, String.t() | nil}
  defp authentication_failed(error) do
    message =
      case error.reason.body do
        %{"error" => %{"message" => message}} when is_binary(message) -> message
        _ -> nil
      end

    {:authentication_failed, message}
  end

  @spec log_ce_error(String.t(), any) :: :ok
  defp log_ce_error(action, error)

  on_ce do
    defp log_ce_error(action, error) do
      Logger.error("Google Analytics 4: Failed when #{action}. Reason: #{inspect(error)}")
    end
  end

  on_ee do
    defp log_ce_error(_action, _error), do: :ok
  end
end
