defmodule NervesHub.Devices.DeviceFiltering do
  @moduledoc """
  Encapsulates all device filtering and sorting logic
  """

  import Ecto.Query

  alias NervesHub.Devices.Alarms
  alias NervesHub.Devices.DeviceMetric
  alias NervesHub.Devices.InflightUpdate
  alias NervesHub.Types.Tag

  @spec build_filters(Ecto.Query.t(), %{optional(atom) => String.t()}) :: Ecto.Query.t()
  def build_filters(query, filters) do
    Enum.reduce(filters, query, fn {key, value}, query ->
      filter(query, filters, key, value)
    end)
  end

  @spec filter(Ecto.Query.t(), %{optional(atom) => String.t()}, atom, String.t()) ::
          Ecto.Query.t()
  def filter(query, filters, key, value)

  # Filter values are empty strings as default,
  # they should be ignored.
  def filter(query, _filters, _key, "") do
    query
  end

  def filter(query, _filters, :alarm, value) do
    where(query, [d], d.id in subquery(Alarms.query_devices_with_alarm(value)))
  end

  def filter(query, _filters, :alarm_status, value) do
    case value do
      "with" -> where(query, [d], d.id in subquery(Alarms.query_devices_with_alarms()))
      "without" -> where(query, [d], d.id not in subquery(Alarms.query_devices_with_alarms()))
      _ -> query
    end
  end

  def filter(query, _filters, :health_status, "unknown") do
    where(query, [latest_health: lh], lh.status == ^"unknown" or is_nil(lh))
  end

  def filter(query, _filters, :health_status, value) do
    where(query, [latest_health: lh], lh.status == ^value)
  end

  def filter(query, _filters, :connection, value) do
    if value == "not_seen" do
      where(query, [d], d.status == :registered)
    else
      where(query, [latest_connection: lc], lc.status == ^value)
    end
  end

  def filter(query, _filters, :connection_type, value) do
    where(
      query,
      [latest_connection: lc],
      fragment("?::jsonb <@ ?", ^[value], lc.metadata["connection_types"])
    )
  end

  def filter(query, _filters, :firmware_version, value) do
    where(query, [d], d.firmware_metadata["version"] == ^value)
  end

  def filter(query, _filters, :platform, value) do
    if value == "Unknown" do
      where(query, [d], is_nil(d.firmware_metadata["platform"]))
    else
      where(query, [d], d.firmware_metadata["platform"] == ^value)
    end
  end

  def filter(query, _filters, :updates, value) do
    case value do
      "enabled" ->
        where(query, [d], d.updates_enabled == true)

      "penalty-box" ->
        where(query, [d], d.updates_blocked_until > fragment("now() at time zone 'utc'"))

      "disabled" ->
        where(query, [d], d.updates_enabled == false)
    end
  end

  def filter(query, _filters, :identifier, value) do
    where(query, [d], ilike(d.identifier, ^"%#{value}%"))
  end

  def filter(query, _filters, :deployment_id, nil) do
    where(query, [d], is_nil(d.deployment_id))
  end

  def filter(query, _filters, :deployment_id, value) do
    where(query, [d], d.deployment_id == ^value)
  end

  def filter(query, _filters, :tags, value) do
    build_tag_filter(query, value)
  end

  def filter(query, _filters, :has_no_tags, value) do
    if value do
      where(query, [d], fragment("array_length(?, 1) = 0 or ? IS NULL", d.tags, d.tags))
    else
      query
    end
  end

  def filter(query, filters, :metrics_key, _value) do
    filter_on_metric(query, filters)
  end

  def filter(query, _filters, :display_deleted, "include"), do: order_by(query, [d], asc_nulls_last: d.deleted_at)

  def filter(query, _filters, :display_deleted, "exclude"), do: where(query, [d], is_nil(d.deleted_at))

  def filter(query, _filters, :display_deleted, "only"), do: where(query, [d], not is_nil(d.deleted_at))

  def filter(query, _filters, :only_updating, false), do: query

  def filter(query, _filters, :only_updating, true),
    do: join(query, :inner, [d], iu in InflightUpdate, on: d.id == iu.device_id, as: :inflight_update)

  def filter(query, _filters, :search, value) when is_binary(value) and value != "" do
    search_term = "%#{value}%"

    query
    |> where(
      [d],
      ilike(d.identifier, ^search_term) or
        ilike(fragment("COALESCE(?->>'version', '')", d.firmware_metadata), ^search_term) or
        ilike(fragment("COALESCE(?->>'platform', '')", d.firmware_metadata), ^search_term) or
        fragment(
          "string_array_to_string(COALESCE(?, ARRAY[]::text[]), ' ', ' ') ILIKE ?",
          d.tags,
          ^search_term
        )
    )
  end

  # Ignore any undefined filter.
  # This will prevent error 500 responses on deprecated saved bookmarks etc.
  def filter(query, _filters, _key, _value) do
    query
  end

  @spec sort(Ecto.Query.t(), {atom(), atom()}) :: Ecto.Query.t()
  def sort(query, {:asc, :connection_established_at}) do
    order_by(query, [latest_connection: latest_connection], desc_nulls_last: latest_connection.established_at)
  end

  def sort(query, {:desc, :connection_established_at}) do
    order_by(query, [latest_connection: latest_connection], asc_nulls_first: latest_connection.established_at)
  end

  def sort(query, {:asc, :connection_last_seen_at}) do
    order_by(query, [latest_connection: latest_connection], desc_nulls_last: latest_connection.last_seen_at)
  end

  def sort(query, {:desc, :connection_last_seen_at}) do
    order_by(query, [latest_connection: latest_connection], asc_nulls_first: latest_connection.last_seen_at)
  end

  def sort(query, sort), do: order_by(query, [], ^sort)

  defp build_tag_filter(query, value) do
    case Tag.cast(value) do
      {:ok, tags} ->
        Enum.reduce(tags, query, fn tag, query ->
          where(
            query,
            [d],
            fragment("string_array_to_string(?, ' ', ' ') ILIKE ?", d.tags, ^"%#{tag}%")
          )
        end)

      {:error, _} ->
        query
    end
  end

  defp filter_on_metric(query, %{metrics_key: key, metrics_operator: operator, metrics_value: value})
       when key != "" and value != "" do
    {value_as_float, _} = Float.parse(value)

    query
    |> join(:inner, [d], m in DeviceMetric, on: d.id == m.device_id, as: :device_metric)
    |> where([device_metric: dm], dm.inserted_at == subquery(latest_metric_for_key(key)))
    |> where([device_metric: dm], dm.key == ^key)
    |> gt_or_lt(value_as_float, operator)
  end

  defp filter_on_metric(query, _), do: query

  defp latest_metric_for_key(key) do
    DeviceMetric
    |> select([dm], max(dm.inserted_at))
    |> where([dm], dm.key == ^key)
  end

  defp gt_or_lt(query, value, "gt"), do: where(query, [device_metric: dm], dm.value > ^value)
  defp gt_or_lt(query, value, "lt"), do: where(query, [device_metric: dm], dm.value < ^value)
end
