defmodule Mobilizon.Service.Geospatial.Nominatim do
  @moduledoc """
  [Nominatim](https://wiki.openstreetmap.org/wiki/Nominatim) backend.
  """
  alias Mobilizon.Service.Geospatial.Provider
  alias Mobilizon.Addresses.Address
  require Logger

  @behaviour Provider

  @endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
  @api_key Application.get_env(:mobilizon, __MODULE__) |> get_in([:api_key])

  @impl Provider
  @doc """
  Nominatim implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
  """
  @spec geocode(String.t(), keyword()) :: list(Address.t())
  def geocode(lon, lat, options \\ []) do
    url = build_url(:geocode, %{lon: lon, lat: lat}, options)
    Logger.debug("Asking Nominatim for geocode with #{url}")

    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
           HTTPoison.get(url),
         {:ok, body} <- Poison.decode(body) do
      [process_data(body)]
    end
  end

  @impl Provider
  @doc """
  Nominatim implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
  """
  @spec search(String.t(), keyword()) :: list(Address.t())
  def search(q, options \\ []) do
    url = build_url(:search, %{q: q}, options)
    Logger.debug("Asking Nominatim for addresses with #{url}")

    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
           HTTPoison.get(url),
         {:ok, body} <- Poison.decode(body) do
      body |> Enum.map(fn entry -> process_data(entry) end) |> Enum.filter(& &1)
    end
  end

  @spec build_url(atom(), map(), list()) :: String.t()
  defp build_url(method, args, options) do
    limit = Keyword.get(options, :limit, 10)
    lang = Keyword.get(options, :lang, "en")
    endpoint = Keyword.get(options, :endpoint, @endpoint)
    api_key = Keyword.get(options, :api_key, @api_key)

    url =
      case method do
        :search ->
          "#{endpoint}/search?format=jsonv2&q=#{URI.encode(args.q)}&limit=#{limit}&accept-language=#{
            lang
          }&addressdetails=1"

        :geocode ->
          "#{endpoint}/reverse?format=jsonv2&lat=#{args.lat}&lon=#{args.lon}&addressdetails=1"
      end

    if is_nil(api_key), do: url, else: url <> "&key=#{api_key}"
  end

  @spec process_data(map()) :: Address.t()
  defp process_data(%{"address" => address} = body) do
    try do
      %Address{
        country: Map.get(address, "country"),
        locality: Map.get(address, "city"),
        region: Map.get(address, "state"),
        description: description(body),
        floor: Map.get(address, "floor"),
        geom: [Map.get(body, "lon"), Map.get(body, "lat")] |> Provider.coordinates(),
        postal_code: Map.get(address, "postcode"),
        street: street_address(address),
        origin_id: "osm:" <> to_string(Map.get(body, "osm_id"))
      }
    rescue
      e in ArgumentError ->
        Logger.warn(inspect(e))
        nil
    end
  end

  @spec street_address(map()) :: String.t()
  defp street_address(body) do
    road =
      cond do
        Map.has_key?(body, "road") ->
          Map.get(body, "road")

        Map.has_key?(body, "road") ->
          Map.get(body, "road")

        Map.has_key?(body, "pedestrian") ->
          Map.get(body, "pedestrian")

        true ->
          ""
      end

    Map.get(body, "house_number", "") <> " " <> road
  end

  @address29_classes ["amenity", "shop", "tourism", "leisure"]
  @address29_categories ["office"]

  @spec description(map()) :: String.t()
  defp description(body) do
    if !Map.has_key?(body, "display_name") do
      Logger.warn("Address has no display name")
      raise ArgumentError, message: "Address has no display_name"
    end

    description = Map.get(body, "display_name")
    address = Map.get(body, "address")

    if (Map.get(body, "category") in @address29_categories or
          Map.get(body, "class") in @address29_classes) and Map.has_key?(address, "address29") do
      Map.get(address, "address29")
    else
      description
    end
  end
end