defmodule Observer.Web.Tracing.Page do
  @moduledoc """
  This is the live component responsible for handling the Tracing debug
  """

  @behaviour Observer.Web.Page

  use Observer.Web, :live_component

  alias Observer.Web.Components.Attention
  alias Observer.Web.Components.CopyToClipboard
  alias Observer.Web.Components.Core
  alias Observer.Web.Components.MultiSelectList
  alias Observer.Web.Page
  alias ObserverWeb.Tracer

  @impl Phoenix.LiveComponent
  def render(assigns) do
    unselected_services_keys =
      assigns.node_info.services_keys -- assigns.node_info.selected_services_keys

    unselected_modules_keys =
      assigns.node_info.modules_keys -- assigns.node_info.selected_modules_keys

    unselected_functions_keys =
      assigns.node_info.functions_keys -- assigns.node_info.selected_functions_keys

    unselected_match_spec_keys =
      assigns.node_info.match_spec_keys -- assigns.node_info.selected_match_spec_keys

    trace_state = Tracer.state()

    trace_idle? = trace_state.status == :idle
    trace_owner? = trace_state.session_id == assigns.trace_session_id

    # Hide options when running
    show_tracing_options = trace_idle? and trace_owner? and assigns.show_tracing_options

    match_spec_info = ~H"""
    <a
      href="https://www.erlang.org/docs/24/apps/erts/match_spec"
      class="font-medium text-blue-600 underline dark:text-blue-500 hover:no-underline"
    >
      (more_info)
    </a>
    """

    attention_msg = ~H"""
    Incorrect use of the <b>:dbg</b>
    tracer in production can lead to performance degradation, latency and crashes.
    <b>Observer Web tracing</b> enforces limits on the maximum number of messages and applies a timeout (in seconds)
    to ensure the debugger doesn't remain active unintentionally. Check out the
    <a
      href="https://www.erlang.org/docs/24/man/dbg"
      class="font-medium text-blue-600 underline dark:text-blue-500 hover:no-underline"
    >
      Erlang Debugger
    </a> for more detailed information.
    """

    assigns =
      assigns
      |> assign(unselected_services_keys: unselected_services_keys)
      |> assign(unselected_modules_keys: unselected_modules_keys)
      |> assign(unselected_functions_keys: unselected_functions_keys)
      |> assign(unselected_match_spec_keys: unselected_match_spec_keys)
      |> assign(match_spec_info: match_spec_info)
      |> assign(trace_idle?: trace_idle?)
      |> assign(trace_idle?: trace_idle?)
      |> assign(trace_owner?: trace_owner?)
      |> assign(show_tracing_options: show_tracing_options)
      |> assign(attention_msg: attention_msg)

    ~H"""
    <div class="min-h-screen bg-white dark:bg-gray-800">
      <Attention.content
        id="tracing"
        title="Attention"
        class="border-red-400 dark:border-red-700 text-red-500 dark:text-red-200"
        message={@attention_msg}
      >
        <:inner_form>
          <.form
            for={@form}
            id="tracing-update-form"
            class="flex ml-2 mr-2 text-xs text-center text-zinc-800 dark:text-white  whitespace-nowrap gap-5"
            phx-change="form-update"
          >
            <Core.input
              field={@form[:max_messages]}
              type="number"
              step="1"
              min="1"
              max="50"
              label="Messages"
            />

            <Core.input
              field={@form[:session_timeout_seconds]}
              type="number"
              step="15"
              min="30"
              max="300"
              label="Timeout(s)"
            />
          </.form>
        </:inner_form>
        <:inner_button>
          <button
            :if={@trace_idle? and @trace_owner?}
            id="tracing-multi-select-run"
            phx-click="tracing-apps-run"
            class="phx-submit-loading:opacity-75 rounded-r-xl bg-green-500 dark:bg-green-700 transform active:scale-75 transition-transform hover:bg-green-800 dark:hover:bg-green-800 py-10 w-64 text-sm font-semibold text-white active:text-white/80"
          >
            RUN
          </button>
          <button
            :if={@trace_idle? == false and @trace_owner?}
            id="tracing-multi-select-stop"
            phx-click="tracing-apps-stop"
            class="phx-submit-loading:opacity-75 rounded-r-xl bg-red-500 dark:bg-red-700 transform active:scale-75 transition-transform hover:bg-red-600 dark:hover:bg-red-800 py-10 w-64 text-sm font-semibold text-white active:text-white/80 animate-pulse"
          >
            STOP
          </button>

          <button
            :if={not @trace_owner?}
            class="phx-submit-loading:opacity-75 rounded-r-xl bg-red-500 dark:bg-red-700 transform active:scale-75 transition-transform hover:bg-red-600 dark:hover:bg-red-800 py-10 w-64 text-sm font-semibold text-white active:text-white/80 animate-pulse"
          >
            IN USE
          </button>
        </:inner_button>
      </Attention.content>
      <div class="flex">
        <MultiSelectList.content
          id="tracing-multi-select"
          selected_text="Selected Items"
          selected={[
            %{name: "services", keys: @node_info.selected_services_keys},
            %{name: "modules", keys: @node_info.selected_modules_keys},
            %{name: "functions", keys: @node_info.selected_functions_keys},
            %{name: "match_spec", keys: @node_info.selected_match_spec_keys}
          ]}
          unselected={[
            %{name: "services", keys: @unselected_services_keys},
            %{name: "modules", keys: @unselected_modules_keys},
            %{name: "functions", keys: @unselected_functions_keys},
            %{name: "match_spec", keys: @unselected_match_spec_keys, info: @match_spec_info}
          ]}
          show_options={@show_tracing_options}
          form_search={@form_search}
        />
      </div>
      <div class="p-2">
        <div class="bg-white dark:bg-gray-800 w-full shadow-lg rounded">
          <Core.table_tracing id="live-logs" rows={@streams.tracing_messages}>
            <:col :let={{_id, tracing_message}} label="SERVICE">
              <span>{tracing_message.service}</span>
            </:col>
            <:col :let={{_id, tracing_message}} label="INDEX">
              <span>{tracing_message.index}</span>
            </:col>
            <:col :let={{_id, tracing_message}} label="TYPE">
              <span>{tracing_message.type}</span>
            </:col>
            <:col :let={{id, tracing_message}} label="CONTENT">
              <div class="flex items-center justify-between gap-2">
                {tracing_message.content}

                <CopyToClipboard.content
                  id={"tracing-functions-messages-#{id}"}
                  message={tracing_message.content}
                />
              </div>
            </:col>
          </Core.table_tracing>
        </div>
      </div>
    </div>
    """
  end

  @impl Page
  def handle_mount(socket) when is_connected?(socket) do
    # Subscribe to notifications if any node is UP or Down
    :net_kernel.monitor_nodes(true)

    socket
    |> assign(:node_info, update_node_info())
    |> assign(:node_data, %{})
    |> assign(:trace_session_id, nil)
    |> assign(:show_tracing_options, false)
    |> assign(:form, to_form(default_form_options()))
    |> assign(:form_search, to_form(default_form_search_options()))
    |> stream(:tracing_messages, [])
  end

  def handle_mount(socket) do
    socket
    |> assign(:node_info, node_info_new())
    |> assign(:node_data, %{})
    |> assign(:trace_session_id, nil)
    |> assign(:show_tracing_options, false)
    |> assign(form: to_form(default_form_options()))
    |> assign(form_search: to_form(default_form_search_options()))
    |> stream(:tracing_messages, [])
  end

  @impl Page
  def handle_params(params, _uri, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Live Tracing")
  end

  @impl Page
  def handle_parent_event("toggle-options", _value, socket) do
    show_tracing_options = !socket.assigns.show_tracing_options

    {:noreply, socket |> assign(:show_tracing_options, show_tracing_options)}
  end

  def handle_parent_event(
        "form-update",
        %{"max_messages" => max_messages, "session_timeout_seconds" => session_timeout_seconds},
        socket
      ) do
    {:noreply,
     assign(socket,
       form:
         to_form(%{
           "max_messages" => max_messages,
           "session_timeout_seconds" => session_timeout_seconds
         })
     )}
  end

  def handle_parent_event(
        "form-multi-select-list-update-search",
        %{"_target" => target} = values,
        socket
      ) do
    new_params = Enum.reduce(target, %{}, fn key, acc -> Map.put(acc, key, values[key]) end)

    form_search =
      socket.assigns.form_search.params
      |> Map.merge(new_params)
      |> to_form()

    {:noreply, assign(socket, form_search: form_search)}
  end

  def handle_parent_event(
        "tracing-apps-stop",
        _data,
        %{assigns: %{trace_session_id: trace_session_id}} = socket
      ) do
    Tracer.stop_trace(trace_session_id)
    {:noreply, assign(socket, :trace_session_id, nil)}
  end

  def handle_parent_event(
        "tracing-apps-run",
        _data,
        %{assigns: %{node_info: node_info, form: form}} = socket
      ) do
    tracer_state = Tracer.state()

    if tracer_state.status == :idle do
      functions_to_monitor =
        Enum.reduce(node_info.selected_services_keys, [], fn service_key, service_acc ->
          service_info = Enum.find(node_info.node, &(&1.service == service_key))

          service_acc ++
            Enum.reduce(node_info.selected_modules_keys, [], fn module_key, module_acc ->
              module_key_atom = String.to_existing_atom(module_key)

              node_functions_info =
                Enum.find(service_info.functions, &(&1.module == module_key_atom))

              functions =
                Enum.reduce(node_info.selected_functions_keys, [], fn function_key,
                                                                      function_acc ->
                  function = Map.get(node_functions_info.functions, function_key, nil)

                  # credo:disable-for-lines:14
                  if module_key in service_info.modules_keys and function do
                    function_acc ++
                      [
                        %{
                          node: String.to_existing_atom(service_key),
                          module: module_key_atom,
                          function: function.name,
                          arity: function.arity,
                          match_spec: node_info.selected_match_spec_keys
                        }
                      ]
                  else
                    function_acc
                  end
                end)

              # If the module doesn't have any of the requested functions the default is to
              # include the whole module
              if functions == [] do
                module_acc ++
                  [
                    %{
                      node: String.to_existing_atom(service_key),
                      module: module_key_atom,
                      function: :_,
                      arity: :_,
                      match_spec: node_info.selected_match_spec_keys
                    }
                  ]
              else
                module_acc ++ functions
              end
            end)
        end)

      case Tracer.start_trace(functions_to_monitor, %{
             max_messages: String.to_integer(form.params["max_messages"]),
             session_timeout_ms: String.to_integer(form.params["session_timeout_seconds"]) * 1_000
           }) do
        {:ok, %{session_id: session_id}} ->
          {:noreply,
           socket
           |> assign(:trace_session_id, session_id)
           |> stream(:tracing_messages, [], reset: true)}

        # coveralls-ignore-start
        {:error, _} ->
          {:noreply, assign(socket, :trace_session_id, nil)}
          # coveralls-ignore-stop
      end
    else
      {:noreply, assign(socket, :trace_session_id, nil)}
    end
  end

  def handle_parent_event(
        "multi-select-remove-item",
        %{"item" => "services", "key" => service_key},
        %{assigns: %{node_info: node_info}} = socket
      ) do
    node_info =
      update_node_info(
        node_info.selected_services_keys -- [service_key],
        node_info.selected_modules_keys,
        node_info.selected_functions_keys,
        node_info.selected_match_spec_keys
      )

    {:noreply, assign(socket, :node_info, node_info)}
  end

  def handle_parent_event(
        "multi-select-remove-item",
        %{"item" => "modules", "key" => module_key},
        %{assigns: %{node_info: node_info}} = socket
      ) do
    node_info =
      update_node_info(
        node_info.selected_services_keys,
        node_info.selected_modules_keys -- [module_key],
        node_info.selected_functions_keys,
        node_info.selected_match_spec_keys
      )

    {:noreply, assign(socket, :node_info, node_info)}
  end

  def handle_parent_event(
        "multi-select-remove-item",
        %{"item" => "functions", "key" => function_key},
        %{assigns: %{node_info: node_info}} = socket
      ) do
    node_info =
      update_node_info(
        node_info.selected_services_keys,
        node_info.selected_modules_keys,
        node_info.selected_functions_keys -- [function_key],
        node_info.selected_match_spec_keys
      )

    {:noreply, assign(socket, :node_info, node_info)}
  end

  def handle_parent_event(
        "multi-select-remove-item",
        %{"item" => "match_spec", "key" => match_spec_key},
        %{assigns: %{node_info: node_info}} = socket
      ) do
    node_info =
      update_node_info(
        node_info.selected_services_keys,
        node_info.selected_modules_keys,
        node_info.selected_functions_keys,
        node_info.selected_match_spec_keys -- [match_spec_key]
      )

    {:noreply, assign(socket, :node_info, node_info)}
  end

  def handle_parent_event(
        "multi-select-add-item",
        %{"item" => "services", "key" => service_key},
        %{assigns: %{node_info: node_info}} = socket
      ) do
    node_info =
      update_node_info(
        node_info.selected_services_keys ++ [service_key],
        node_info.selected_modules_keys,
        node_info.selected_functions_keys,
        node_info.selected_match_spec_keys
      )

    {:noreply, assign(socket, :node_info, node_info)}
  end

  def handle_parent_event(
        "multi-select-add-item",
        %{"item" => "modules", "key" => module_key},
        %{assigns: %{node_info: node_info}} = socket
      ) do
    node_info =
      update_node_info(
        node_info.selected_services_keys,
        node_info.selected_modules_keys ++ [module_key],
        node_info.selected_functions_keys,
        node_info.selected_match_spec_keys
      )

    {:noreply, assign(socket, :node_info, node_info)}
  end

  def handle_parent_event(
        "multi-select-add-item",
        %{"item" => "functions", "key" => function_key},
        %{assigns: %{node_info: node_info}} = socket
      ) do
    node_info =
      update_node_info(
        node_info.selected_services_keys,
        node_info.selected_modules_keys,
        node_info.selected_functions_keys ++ [function_key],
        node_info.selected_match_spec_keys
      )

    {:noreply, assign(socket, :node_info, node_info)}
  end

  def handle_parent_event(
        "multi-select-add-item",
        %{"item" => "match_spec", "key" => match_spec_key},
        %{assigns: %{node_info: node_info}} = socket
      ) do
    node_info =
      update_node_info(
        node_info.selected_services_keys,
        node_info.selected_modules_keys,
        node_info.selected_functions_keys,
        node_info.selected_match_spec_keys ++ [match_spec_key]
      )

    {:noreply, assign(socket, :node_info, node_info)}
  end

  @impl Page
  def handle_info({:nodeup, _node}, %{assigns: %{node_info: node_info}} = socket) do
    node_info =
      update_node_info(
        node_info.selected_services_keys,
        node_info.selected_modules_keys,
        node_info.selected_functions_keys,
        node_info.selected_match_spec_keys
      )

    {:noreply, assign(socket, :node_info, node_info)}
  end

  def handle_info({:nodedown, node}, %{assigns: %{node_info: node_info}} = socket) do
    service_key = node |> to_string

    node_info =
      update_node_info(
        node_info.selected_services_keys -- [service_key],
        node_info.selected_modules_keys,
        node_info.selected_functions_keys,
        node_info.selected_match_spec_keys
      )

    {:noreply, assign(socket, :node_info, node_info)}
  end

  def handle_info({:new_trace_message, _session_id, node, index, type, message}, socket) do
    data = %{
      service: node,
      id: ObserverWeb.Common.uuid4(),
      index: index,
      type: type,
      content: message
    }

    {:noreply, stream(socket, :tracing_messages, [data])}
  end

  def handle_info({event, _session_id}, socket)
      when event in [:trace_session_timeout, :stop_tracing] do
    {:noreply,
     socket
     |> assign(:trace_session_id, nil)
     |> assign(:show_tracing_options, false)}
  end

  defp default_form_options do
    %{
      "max_messages" => "3",
      "session_timeout_seconds" => "30"
    }
  end

  defp default_form_search_options, do: %{"modules" => "", "functions" => ""}

  defp node_info_new do
    match_spec_keys =
      Tracer.get_default_functions_matchspecs()
      |> Map.keys()
      |> Enum.sort()

    %{
      services_keys: [],
      modules_keys: [],
      functions_keys: [],
      match_spec_keys: match_spec_keys,
      selected_services_keys: [],
      selected_modules_keys: [],
      selected_functions_keys: [],
      selected_match_spec_keys: [],
      node: []
    }
  end

  defp update_node_info, do: update_node_info([], [], [], [])

  defp update_node_info(
         selected_services_keys,
         selected_modules_keys,
         selected_functions_keys,
         selected_match_spec_keys
       ) do
    initial_map =
      %{
        node_info_new()
        | selected_services_keys: selected_services_keys,
          selected_modules_keys: selected_modules_keys,
          selected_functions_keys: selected_functions_keys,
          selected_match_spec_keys: selected_match_spec_keys
      }

    Enum.reduce(Node.list() ++ [Node.self()], initial_map, fn instance_node,
                                                              %{
                                                                services_keys: services_keys,
                                                                modules_keys: modules_keys,
                                                                functions_keys: functions_keys,
                                                                match_spec_keys: match_spec_keys,
                                                                node: node
                                                              } = acc ->
      service = instance_node |> to_string
      service_selected? = service in selected_services_keys

      [name, _hostname] = String.split(service, "@")
      services_keys = (services_keys ++ [service]) |> Enum.sort()

      instance_module_keys =
        if service_selected? do
          Tracer.get_modules(instance_node) |> Enum.map(&to_string/1)
        else
          []
        end

      {instance_functions_keys, functions} =
        Enum.reduce(instance_module_keys, {[], []}, fn module, {keys, fun} ->
          # credo:disable-for-lines:6
          if module in selected_modules_keys do
            module_functions_info =
              Tracer.get_module_functions_info(instance_node, String.to_existing_atom(module))

            function_keys = Map.keys(module_functions_info.functions) |> Enum.map(&to_string/1)
            {keys ++ function_keys, fun ++ [module_functions_info]}
          else
            {keys, fun}
          end
        end)

      modules_keys = (modules_keys ++ instance_module_keys) |> Enum.sort() |> Enum.uniq()
      functions_keys = (functions_keys ++ instance_functions_keys) |> Enum.sort() |> Enum.uniq()

      node =
        if service_selected? do
          [
            %{
              name: name,
              modules_keys: instance_module_keys,
              function_keys: instance_functions_keys,
              match_spec_keys: match_spec_keys,
              service: service,
              functions: functions
            }
            | node
          ]
        else
          node
        end

      %{
        acc
        | services_keys: services_keys,
          modules_keys: modules_keys,
          functions_keys: functions_keys,
          match_spec_keys: match_spec_keys,
          node: node
      }
    end)
  end
end
