defmodule FunWithFlags.UI.Router do
  @moduledoc """
  A `Plug.Router`. This module is meant to be plugged into host applications.

  See the [Readme](/fun_with_flags_ui/readme.html#how-to-run) for more detailed instructions.
  """
  require Logger
  use Plug.Router
  alias FunWithFlags.UI.{SimpleActor, Templates, Utils}

  if Mix.env == :dev do
    use Plug.Debugger, otp_app: :fun_with_flags_ui
  end

  plug Plug.Logger, log: :debug

  plug Plug.Static,
    gzip: true,
    at: "/assets",
    from: :fun_with_flags_ui

  plug :protect_from_forgery, Plug.CSRFProtection.init([])

  plug Plug.Parsers, parsers: [:urlencoded]
  plug Plug.MethodOverride

  plug :assign_csrf_token
  plug :match
  plug :dispatch

  @doc false
  def call(conn, opts) do
    conn = extract_namespace(conn, opts)
    super(conn, opts)
  end


  get "/" do
    conn
    |> redirect_to("/flags")
  end


  # form to create a new flag
  #
  get "/new" do
    conn
    |> html_resp(200, Templates.new(%{conn: conn}))
  end


  # endpoint to create a new flag
  #
  post "/flags" do
    name = Utils.sanitize(conn.params["flag_name"])

    case Utils.validate_flag_name(conn, name) do
      :ok ->
        case Utils.create_flag_with_name(name) do
          {:ok, _} -> redirect_to conn, "/flags/#{name}"
          _ -> html_resp(conn, 400, Templates.new(%{conn: conn, error_message: "Something went wrong!"}))
        end
      {:fail, reason} ->
        html_resp(conn, 400, Templates.new(%{conn: conn, error_message: reason}))
    end
  end


  # get a list of the flags
  #
  get "/flags" do
    {:ok, flags} = FunWithFlags.all_flags
    flags = Utils.sort_flags(flags)
    body = Templates.index(conn: conn, flags: flags)

    conn
    |> html_resp(200, body)
  end


  # flag details
  #
  get "/flags/:name" do
    case Utils.get_flag(name) do
      {:ok, flag} ->
        body = Templates.details(conn: conn, flag: flag)
        html_resp(conn, 200, body)
      {:error, _} ->
        body = Templates.not_found(conn: conn, name: name)
        html_resp(conn, 404, body)
    end
  end


  # to clear an entire flag
  #
  delete "/flags/:name" do
    name
    |> String.to_existing_atom()
    |> FunWithFlags.clear()

    redirect_to conn, "/flags"
  end


  # to toggle the default state of a flag
  #
  patch "/flags/:name/boolean" do
    enabled = Utils.parse_bool(conn.params["enabled"])
    flag_name = String.to_existing_atom(name)

    if enabled do
      FunWithFlags.enable(flag_name)
    else
      FunWithFlags.disable(flag_name)
    end

    redirect_to conn, "/flags/#{name}"
  end


  # to clear a boolean gate
  #
  delete "/flags/:name/boolean" do
    flag_name = String.to_existing_atom(name)
    FunWithFlags.clear(flag_name, boolean: true)
    redirect_to conn, "/flags/#{name}"
  end


  # to toggle an actor gate
  #
  patch "/flags/:name/actors/:actor_id" do
    enabled = Utils.parse_bool(conn.params["enabled"])
    flag_name = String.to_existing_atom(name)
    actor = %SimpleActor{id: actor_id}

    if enabled do
      FunWithFlags.enable(flag_name, for_actor: actor)
    else
      FunWithFlags.disable(flag_name, for_actor: actor)
    end

    redirect_to conn, "/flags/#{name}#actor_#{actor_id}"
  end


  # to clear an actor gate
  #
  delete "/flags/:name/actors/:actor_id" do
    flag_name = String.to_existing_atom(name)
    actor = %SimpleActor{id: actor_id}

    FunWithFlags.clear(flag_name, for_actor: actor)
    redirect_to conn, "/flags/#{name}#actor_gates"
  end


  # to toggle a group gate
  #
  patch "/flags/:name/groups/:group_name" do
    enabled = Utils.parse_bool(conn.params["enabled"])
    flag_name = String.to_existing_atom(name)
    group_name = to_string(group_name)

    if enabled do
      FunWithFlags.enable(flag_name, for_group: group_name)
    else
      FunWithFlags.disable(flag_name, for_group: group_name)
    end

    redirect_to conn, "/flags/#{name}#group_#{group_name}"
  end


  # to clear a group gate
  #
  delete "/flags/:name/groups/:group_name" do
    flag_name = String.to_existing_atom(name)
    group_name = to_string(group_name)

    FunWithFlags.clear(flag_name, for_group: group_name)

    redirect_to conn, "/flags/#{name}#group_gates"
  end


  # to clear a percentage gate
  #
  delete "/flags/:name/percentage" do
    flag_name = String.to_existing_atom(name)
    FunWithFlags.clear(flag_name, for_percentage: true)
    redirect_to conn, "/flags/#{name}"
  end


  # to add a new actor to a flag
  #
  post "/flags/:name/actors" do
    flag_name = String.to_existing_atom(name)
    actor_id = Utils.sanitize(conn.params["actor_id"])

    case Utils.validate(actor_id) do
      :ok ->
        enabled = Utils.parse_bool(conn.params["enabled"])
        actor = %SimpleActor{id: actor_id}
        if enabled do
          FunWithFlags.enable(flag_name, for_actor: actor)
        else
          FunWithFlags.disable(flag_name, for_actor: actor)
        end
        redirect_to conn, "/flags/#{name}#actor_#{actor_id}"
      {:fail, reason} ->
        {:ok, flag} = Utils.get_flag(name)
        body = Templates.details(conn: conn, flag: flag, actor_error_message: "The actor ID #{reason}.")
        html_resp(conn, 400, body)
    end
  end


  # to add a new group to a flag
  #
  post "/flags/:name/groups" do
    flag_name = String.to_existing_atom(name)
    group_name = Utils.sanitize(conn.params["group_name"])

    case Utils.validate(group_name) do
      :ok ->
        enabled = Utils.parse_bool(conn.params["enabled"])
        if enabled do
          FunWithFlags.enable(flag_name, for_group: group_name)
        else
          FunWithFlags.disable(flag_name, for_group: group_name)
        end
        redirect_to conn, "/flags/#{name}#group_#{group_name}"
      {:fail, reason} ->
        {:ok, flag} = Utils.get_flag(name)
        body = Templates.details(conn: conn, flag: flag, group_error_message: "The group name #{reason}.")
        html_resp(conn, 400, body)
    end
  end


  # to add or replace a percentage gate
  #
  post "/flags/:name/percentage" do
    flag_name = String.to_existing_atom(name)
    type = Utils.parse_percentage_type(conn.params["percent_type"])

    case Utils.parse_and_validate_float(conn.params["percent_value"]) do
      {:ok, float} ->
        FunWithFlags.enable(flag_name, for_percentage_of: {type, float})
        redirect_to conn, "/flags/#{name}#percentage_gate"
      {:fail, reason} ->
        {:ok, flag} = Utils.get_flag(name)
        body = Templates.details(conn: conn, flag: flag, percentage_error_message: "The percentage value #{reason}.")
        html_resp(conn, 400, body)
    end
  end


  match _ do
    send_resp(conn, 404, "")
  end


  defp html_resp(conn, status, body) do
    conn
    |> put_resp_content_type("text/html")
    |> send_resp(status, body)
  end


  defp redirect_to(conn, uri) do
    path = Path.join(conn.assigns[:namespace], uri)

    conn
    |> put_resp_header("location", path)
    |> put_resp_content_type("text/html")
    |> send_resp(302, "<html><body>You are being <a href=\"#{path}\">redirected</a>.</body></html>")
  end


  defp extract_namespace(conn, opts) do
    ns = opts[:namespace] || ""
    Plug.Conn.assign(conn, :namespace, "/" <> ns)
  end


  defp assign_csrf_token(conn, _opts) do
    csrf_token = Plug.CSRFProtection.get_csrf_token()
    Plug.Conn.assign(conn, :csrf_token, csrf_token)
  end


  # Custom CSRF protection plug. It wraps the default plug provided
  # by `Plug`, it calls `Plug.Conn.fetch_session/1` (no-op if already
  # fetched), and it bails out gracefully if no session is configured.
  #
  defp protect_from_forgery(conn, opts) do
    try do
      conn
      |> fetch_session()
      |> Plug.CSRFProtection.call(opts)
    rescue
      _e in ArgumentError ->
        Logger.warn("CSRF protection won't work unless your host application uses the session plug")
        conn
    end
  end
end
