defmodule Mobilizon.Web.FeedController do
  @moduledoc """
  Controller to serve RSS, ATOM and iCal Feeds
  """
  use Mobilizon.Web, :controller
  plug(:put_layout, false)
  action_fallback(Mobilizon.Web.FallbackController)
  alias Mobilizon.Config
  alias Mobilizon.GraphQL.API.Search
  alias Mobilizon.Service.Export.Feed
  alias Mobilizon.Service.Export.ICalendar
  require Logger

  @spec instance(Plug.Conn.t(), map()) :: Plug.Conn.t()
  def instance(conn, %{"format" => format}) do
    if Config.get([:instance, :enable_instance_feeds], false) do
      return_data(conn, format, "instance", Config.instance_name())
    else
      send_resp(conn, 401, "Instance feeds are not enabled.")
    end
  end

  @spec search(Plug.Conn.t(), map()) :: Plug.Conn.t()
  def search(conn, %{"format" => format}) do
    if Config.get([:instance, :enable_search_feeds], false) do
      args = build_search_args(conn)

      data =
        args
        |> Search.search_events()
        |> case do
          {:ok, page} -> page.elements
          _ -> {:error, "Failed to fetch events"}
        end

      case format do
        "ics" ->
          {:ok, events} = ICalendar.export_events(data)

          conn
          |> put_resp_content_type("text/calendar")
          |> put_resp_header(
            "content-disposition",
            "attachment; filename=\"search.ics\""
          )
          |> send_resp(200, events)

        "atom" ->
          feed_title =
            gettext("Custom search on %{instance}",
              instance: Config.instance_name()
            )

          events = Feed.export_events(data, Phoenix.Controller.current_url(conn), feed_title)

          conn
          |> put_resp_content_type("application/atom+xml")
          |> put_resp_header(
            "content-disposition",
            "attachment; filename=\"search.atom\""
          )
          |> send_resp(200, events)

        _ ->
          send_resp(conn, 401, "Only 'ics' or 'atom' methods are supported.")
      end
    else
      send_resp(conn, 401, "Search feeds are not enabled.")
    end
  end

  defp build_search_args(conn) do
    %{
      term: param_string(conn.params, "term"),
      tags: param_string(conn.params, "tags"),
      location: param_string(conn.params, "location"),
      category: param_string(conn.params, "category"),
      bbox: param_string(conn.params, "bbox"),
      category_one_of: param_list_of_strings(conn.params, "category_one_of"),
      language_one_of: param_list_of_strings(conn.params, "language_one_of"),
      boost_languages: param_list_of_strings(conn.params, "boost_languages"),
      radius: param_float(conn.params, "radius", 50.0),
      long_events: param_boolean(conn.params, "long_events", nil),
      zoom: param_integer(conn.params, "zoom", nil),
      status_one_of: param_status_one_of(conn.params, "status_one_of", nil),
      search_target: param_search_target(conn.params, "search_target", :internal),
      type: param_type(conn.params, "type", nil),
      page: 1,
      limit: 100,
      sort_by: :created_at_asc
    }
  end

  defp param_string(params, key) do
    Map.get(params, key, "")
  end

  defp param_list_of_strings(params, key) do
    params
    |> param_string(key)
    |> String.split(",", trim: true)
    |> Enum.map(&String.trim/1)
  end

  defp param_float(params, key, default) do
    params
    |> param_string(key)
    |> String.replace(",", ".")
    |> Float.parse()
    |> case do
      {float, ""} -> float
      _ -> default
    end
  end

  defp param_integer(params, key, default) do
    params
    |> param_string(key)
    |> Integer.parse()
    |> case do
      {int, ""} -> int
      _ -> default
    end
  end

  defp param_boolean(params, key, default) do
    params
    |> param_string(key)
    |> String.downcase()
    |> case do
      "true" -> true
      "1" -> true
      "false" -> false
      "0" -> false
      _ -> default
    end
  end

  defp param_status_one_of(params, key, default) do
    params
    |> param_list_of_strings(key)
    |> Enum.map(fn status ->
      status
      |> String.downcase()
      |> case do
        "confirmed" -> :confirmed
        "tentative" -> :tentative
        "cancelled" -> :cancelled
        _ -> default
      end
    end)
  end

  defp param_search_target(params, key, default) do
    params
    |> param_list_of_strings(key)
    |> Enum.map(fn search ->
      search
      |> String.downcase()
      |> case do
        "internal" -> :internal
        "self" -> :self
        "global" -> :global
        _ -> default
      end
    end)
  end

  defp param_type(params, key, default) do
    params
    |> param_list_of_strings(key)
    |> Enum.map(fn type ->
      type
      |> String.downcase()
      |> case do
        "in_person" -> :in_person
        "online" -> :online
        _ -> default
      end
    end)
  end

  @spec actor(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found}
  def actor(conn, %{"format" => format, "name" => name}) do
    return_data(conn, format, "actor_" <> name, name)
  end

  def actor(_conn, _) do
    {:error, :not_found}
  end

  @spec event(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found}
  def event(conn, %{"uuid" => uuid, "format" => "ics"}) do
    return_data(conn, "ics", "event_" <> uuid, "event")
  end

  def event(_conn, _) do
    {:error, :not_found}
  end

  @spec going(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found}
  def going(conn, %{"token" => token, "format" => format}) do
    return_data(conn, format, "token_" <> token, "events")
  end

  def going(_conn, _) do
    {:error, :not_found}
  end

  @spec return_data(Plug.Conn.t(), String.t(), String.t(), String.t()) ::
          Plug.Conn.t() | {:error, :not_found}
  defp return_data(conn, "atom", key, filename) do
    case Cachex.fetch(:feed, key) do
      {status, data} when status in [:commit, :ok] ->
        conn
        |> put_resp_content_type("application/atom+xml")
        |> put_resp_header(
          "content-disposition",
          "attachment; filename=\"#{filename}.atom\""
        )
        |> send_resp(200, data)

      # No need to log these two
      {:ignore, :actor_not_found} ->
        {:error, :not_found}

      {:ignore, :actor_not_public} ->
        {:error, :not_found}

      err ->
        Logger.warning("Unable to find feed data cached for key #{key}, returned #{inspect(err)}")
        {:error, :not_found}
    end
  end

  defp return_data(conn, "ics", key, filename) do
    case Cachex.fetch(:ics, key) do
      {status, data} when status in [:commit, :ok] ->
        conn
        |> put_resp_content_type("text/calendar")
        |> put_resp_header(
          "content-disposition",
          "attachment; filename=\"#{filename}.ics\""
        )
        |> send_resp(200, data)

      # No need to log these two
      {:ignore, :actor_not_found} ->
        {:error, :not_found}

      {:ignore, :actor_not_public} ->
        {:error, :not_found}

      err ->
        Logger.warning("Unable to find feed data cached for key #{key}, returned #{inspect(err)}")
        {:error, :not_found}
    end
  end

  defp return_data(_conn, _, _, _) do
    {:error, :not_found}
  end
end