defmodule PhilomenaWeb.ImageNavigator do
  alias PhilomenaWeb.ImageSorter
  alias Philomena.Images.{Image, ElasticsearchIndex}
  alias Philomena.Elasticsearch
  alias Philomena.Repo
  import Ecto.Query

  # We get consecutive images by finding all images greater than or less than
  # the current image, and grabbing the FIRST one
  @range_comparison_for_order %{
    "asc" => :gt,
    "desc" => :lt
  }

  # If we didn't reverse for prev, it would be the LAST image, which would
  # make Elasticsearch choke on deep pagination
  @order_for_dir %{
    next: %{"asc" => "asc", "desc" => "desc"},
    prev: %{"asc" => "desc", "desc" => "asc"}
  }

  @range_map %{
    gt: :gte,
    lt: :lte
  }

  def find_consecutive(conn, image, rel, params, compiled_query, compiled_filter) do
    image_index =
      Image
      |> where(id: ^image.id)
      |> preload([:gallery_interactions, tags: :aliases])
      |> Repo.one()
      |> Map.merge(empty_fields())
      |> ElasticsearchIndex.as_json()

    sort_data = ImageSorter.parse_sort(params)

    {sorts, filters} =
      sort_data.sorts
      |> Enum.map(&extract_filters(&1, image_index, rel))
      |> Enum.unzip()

    sorts = sortify(sorts, image_index)
    filters = filterify(filters, image_index)

    Elasticsearch.search_records(
      Image,
      %{
        query: %{
          bool: %{
            must: List.flatten([compiled_query, sort_data.queries, filters]),
            must_not: [
              compiled_filter,
              %{term: %{hidden_from_users: true}},
              hidden_filter(conn.assigns.current_user, conn.params["hidden"])
            ]
          }
        },
        sort: List.flatten(sorts)
      },
      %{page_size: 1},
      Image
    )
    |> Enum.to_list()
    |> case do
      [] -> image
      [next_image] -> next_image
    end
  end

  defp extract_filters(%{"galleries.position" => term} = sort, image, rel) do
    # Extract gallery ID and current position
    gid = term.nested.filter.term["galleries.id"]
    pos = Enum.find(image[:galleries], &(&1.id == gid)).position

    # Sort in the other direction if we are going backwards
    sd = term.order
    order = @order_for_dir[rel][to_string(sd)]
    term = %{term | order: order}
    sort = %{sort | "galleries.position" => term}

    filter = gallery_range_filter(@range_comparison_for_order[order], pos)

    {[sort], [filter]}
  end

  defp extract_filters(sort, image, rel) do
    [{sf, sd}] = Enum.to_list(sort)
    order = @order_for_dir[rel][sd]
    sort = %{sort | sf => order}

    field = String.to_existing_atom(sf)
    filter = range_filter(sf, @range_comparison_for_order[order], image[field])

    cond do
      sf in [:_random, :_score] ->
        {[sort], []}

      true ->
        {[sort], [filter]}
    end
  end

  defp sortify(sorts, _image) do
    List.flatten(sorts)
  end

  defp filterify(filters, image) do
    filters = List.flatten(filters)

    filters =
      filters
      |> Enum.with_index()
      |> Enum.map(fn
        {filter, 0} ->
          filter.this

        {filter, i} ->
          filters_so_far =
            filters
            |> Enum.take(i)
            |> Enum.map(& &1.for_next)

          %{
            bool: %{
              must: [filter.this | filters_so_far]
            }
          }
      end)

    %{
      bool: %{
        should: filters,
        must_not: %{term: %{id: image.id}}
      }
    }
  end

  defp hidden_filter(%{id: id}, param) when param != "1", do: %{term: %{hidden_by_user_ids: id}}
  defp hidden_filter(_user, _param), do: %{match_none: %{}}

  defp range_filter(sf, dir, val) do
    %{
      this: %{range: %{sf => %{dir => parse_val(val)}}},
      next: %{range: %{sf => %{@range_map[dir] => parse_val(val)}}}
    }
  end

  defp gallery_range_filter(dir, val) do
    %{
      this: %{
        nested: %{
          path: :galleries,
          query: %{range: %{"galleries.position" => %{dir => val}}}
        }
      },
      next: %{
        nested: %{
          path: :galleries,
          query: %{range: %{"galleries.position" => %{@range_map[dir] => val}}}
        }
      }
    }
  end

  defp empty_fields do
    %{
      user: nil,
      deleter: nil,
      upvoters: [],
      downvoters: [],
      favers: [],
      hiders: []
    }
  end

  defp parse_val(%NaiveDateTime{} = value), do: NaiveDateTime.to_iso8601(value)
  defp parse_val(value), do: value
end