defmodule TarakanWeb.ExploreLive do
  use TarakanWeb, :live_view

  alias Tarakan.Activity
  alias Tarakan.Infestations
  alias Tarakan.Reputation
  alias TarakanWeb.Presence

  @wire_limit 50
  # A search sweeps a wider slice of history than the visible window.
  @search_scan_limit 200
  @presence_topic "explore:observers"

  # Product language: Reports / Checks. Wire kinds stay :scan / :verdict internally.
  @kinds %{
    "all" => nil,
    "registrations" => :registration,
    "reports" => :scan,
    "checks" => :verdict,
    "comments" => :comment
  }

  @impl true
  def mount(_params, _session, socket) do
    if connected?(socket) do
      Activity.subscribe()
      Reputation.subscribe()
      Phoenix.PubSub.subscribe(Tarakan.PubSub, @presence_topic)
      {:ok, _ref} = Presence.track(self(), @presence_topic, socket.id, %{})
    end

    {:ok,
     socket
     |> assign(:page_title, "Explore")
     |> assign(
       :meta_description,
       "Public record: Reports, Checks, registrations, discussion."
     )
     |> assign(:canonical_path, ~p"/explore")
     |> assign(:kind, "all")
     |> assign(:query, "")
     |> assign(:watcher_count, watcher_count())
     |> assign(:hot_findings, Activity.hot_findings())
     |> assign(:infestations, Infestations.list_infestations(min_repos: 2, days: 30, limit: 5))
     |> load_wire()}
  end

  @impl true
  def handle_event("filter", %{"kind" => kind}, socket) when is_map_key(@kinds, kind) do
    {:noreply, socket |> assign(:kind, kind) |> load_wire()}
  end

  def handle_event("search", params, socket) do
    query = params |> Map.get("q", "") |> String.trim()
    {:noreply, socket |> assign(:query, query) |> load_wire()}
  end

  @impl true
  def handle_info({:activity, entry}, socket) do
    if matches?(entry, socket.assigns.kind, socket.assigns.query) do
      {:noreply, stream_insert(socket, :wire, entry, at: 0, limit: @wire_limit)}
    else
      {:noreply, socket}
    end
  end

  def handle_info({:vote_changed, "canonical_finding", _id}, socket) do
    {:noreply, assign(socket, :hot_findings, Activity.hot_findings())}
  end

  def handle_info({:vote_changed, _subject_type, _id}, socket), do: {:noreply, socket}

  def handle_info(%Phoenix.Socket.Broadcast{event: "presence_diff"}, socket) do
    {:noreply, assign(socket, :watcher_count, watcher_count())}
  end

  defp load_wire(socket) do
    %{kind: kind, query: query} = socket.assigns
    scan_limit = if query == "", do: @wire_limit, else: @search_scan_limit

    # Scope the kind in the query so a rare kind returns its own `scan_limit`
    # rows instead of being crowded out of the merged newest-N window.
    entries =
      scan_limit
      |> Activity.recent(verified_only: false, kind: Map.fetch!(@kinds, kind))
      |> Enum.filter(&matches_query?(&1, query))
      |> Enum.take(@wire_limit)

    stream(socket, :wire, entries, reset: true)
  end

  defp matches?(entry, kind, query) do
    matches_kind?(entry, kind) and matches_query?(entry, query)
  end

  defp matches_kind?(_entry, "all"), do: true
  defp matches_kind?(entry, kind), do: entry.kind == Map.fetch!(@kinds, kind)

  defp matches_query?(_entry, ""), do: true

  defp matches_query?(entry, query) do
    haystack =
      [
        Map.get(entry, :handle),
        Map.get(entry, :finding_title),
        entry.host,
        "#{entry.owner}/#{entry.name}"
      ]
      |> Enum.reject(&is_nil/1)
      |> Enum.join(" ")
      |> String.downcase()

    String.contains?(haystack, String.downcase(query))
  end

  defp watcher_count do
    @presence_topic |> Presence.list() |> map_size()
  end

  # Reviews and verdicts live on the security tab; comments on the finding
  # page; a registration points at the repository itself.
  defp wire_entry_path(%{kind: :registration} = entry) do
    TarakanWeb.RepositoryPaths.repository_path(entry)
  end

  defp wire_entry_path(%{kind: :comment} = entry) do
    "/findings/#{entry.finding_public_id}"
  end

  defp wire_entry_path(entry) do
    TarakanWeb.RepositoryPaths.repository_security_path(entry)
  end

  defp ledger_time(%DateTime{} = datetime) do
    Calendar.strftime(datetime, "%Y-%m-%d %H:%M")
  end

  defp filter_active?(current, value), do: current == value
end
