defmodule TarakanWeb.InfestationLive.Show do
  @moduledoc "One cross-repo infestation pattern: affected repos and instances."
  use TarakanWeb, :live_view

  alias Tarakan.Fixes
  alias Tarakan.Infestations
  alias Tarakan.Policy
  alias TarakanWeb.RepositoryPaths

  @graph_limit 64
  @ledger_page 50

  @impl true
  def mount(%{"pattern_key" => pattern_key}, _session, socket) do
    infestation = Infestations.get_infestation(pattern_key)

    if is_nil(infestation) or infestation.repo_count < 1 do
      raise Ecto.NoResultsError, queryable: Tarakan.Scans.CanonicalFinding
    end

    graph = Infestations.list_pattern_repos_page(pattern_key, limit: @graph_limit)
    ledger = Infestations.list_instances_page(pattern_key, limit: @ledger_page)

    {:ok,
     socket
     |> assign(:page_title, infestation.title)
     |> assign(
       :meta_description,
       "#{infestation.title}. Seen in #{infestation.repo_count} listed repositories on Tarakan."
     )
     |> assign(:canonical_path, ~p"/infestations/#{pattern_key}")
     |> assign(:pattern_key, pattern_key)
     |> assign(:infestation, infestation)
     |> assign(:open_bounties, Tarakan.Market.open_bounties_for_target(:infestation, pattern_key))
     |> assign(:graph_repos, graph.entries)
     |> assign(:graph_hidden, max(infestation.repo_count - length(graph.entries), 0))
     |> stream(:ledger_instances, ledger.entries, reset: true)
     |> assign(:ledger_cursor, ledger.next_cursor)
     |> assign(:ledger_empty?, ledger.entries == [])
     |> assign(
       :can_moderate,
       Policy.allowed?(socket.assigns.current_scope, :moderate)
     )
     |> load_fix_template(pattern_key)}
  end

  # A pattern has a fix template only once one of its instances has actually
  # been fixed, so the whole section is absent until then.
  defp load_fix_template(socket, pattern_key) do
    case Fixes.latest_template(pattern_key) do
      nil ->
        socket
        |> assign(:fix_template, nil)
        |> assign(:propagations, [])
        |> assign(:propagation_targets, 0)

      template ->
        socket
        |> assign(:fix_template, template)
        |> assign(:propagations, Fixes.list_propagations(template))
        |> assign(:propagation_targets, length(Fixes.propagation_targets(template)))
    end
  end

  @impl true
  def handle_event("propagate_fix", _params, socket) do
    template = socket.assigns.fix_template

    cond do
      not socket.assigns.can_moderate ->
        {:noreply, put_flash(socket, :error, "Not authorized.")}

      is_nil(template) ->
        {:noreply, put_flash(socket, :error, "No captured fix to propagate.")}

      true ->
        case Fixes.propagate(socket.assigns.current_scope, template) do
          {:ok, %{opened: opened, failed: failed}} ->
            {:noreply,
             socket
             |> put_flash(:info, "Opened #{opened} fix job(s). Failed #{failed}.")
             |> load_fix_template(socket.assigns.pattern_key)}

          {:error, :unauthorized} ->
            {:noreply, put_flash(socket, :error, "Not authorized.")}

          {:error, _reason} ->
            {:noreply, put_flash(socket, :error, "Could not open fix jobs.")}
        end
    end
  end

  def handle_event("load_more_instances", _params, socket) do
    case socket.assigns.ledger_cursor do
      nil ->
        {:noreply, socket}

      cursor ->
        page =
          Infestations.list_instances_page(socket.assigns.pattern_key,
            limit: @ledger_page,
            cursor: cursor
          )

        {:noreply,
         socket
         |> stream(:ledger_instances, page.entries)
         |> assign(:ledger_cursor, page.next_cursor)}
    end
  end

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash} current_scope={@current_scope} current_path={assigns[:current_path]}>
      <Layouts.page width={:wide}>
        <.breadcrumbs id="infestation-breadcrumb">
          <:crumb navigate={~p"/"}>registry</:crumb>
          <:crumb navigate={~p"/infestations"}>infestations</:crumb>
          <:crumb>pattern</:crumb>
        </.breadcrumbs>

        <TarakanWeb.BountyComponents.wanted_banner bounties={@open_bounties} />

        <div class="mt-4 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
          <div class="min-w-0 max-w-3xl">
            <h1 class="font-display text-2xl font-medium leading-tight tracking-[0.02em] text-ink sm:text-4xl">
              {@infestation.title}
            </h1>
            <p
              :if={@infestation.severity}
              class="mt-2 font-mono text-xs text-signal"
            >
              {@infestation.severity}
            </p>
          </div>
        </div>

        <div class="mt-6 grid gap-px border-2 border-strong bg-rule sm:grid-cols-4">
          <div class="bg-ground px-4 py-4">
            <p class="text-xs font-medium text-ink-faint">Repos</p>
            <p id="infestation-repo-count" class="mt-1 font-display text-3xl tabular-nums text-ink">
              {@infestation.repo_count}
            </p>
          </div>
          <div class="bg-ground px-4 py-4">
            <p class="text-xs font-medium text-ink-faint">Open</p>
            <p class="mt-1 font-display text-3xl tabular-nums text-phosphor">
              {@infestation.open_count}
            </p>
          </div>
          <div class="bg-ground px-4 py-4">
            <p class="text-xs font-medium text-ink-faint">Verified</p>
            <p class="mt-1 font-display text-3xl tabular-nums text-ink">
              {@infestation.verified_count}
            </p>
          </div>
          <div class="bg-ground px-4 py-4">
            <p class="text-xs font-medium text-ink-faint">Fixed</p>
            <p class="mt-1 font-display text-3xl tabular-nums text-ink-muted">
              {@infestation.fixed_count}
            </p>
          </div>
        </div>

        <%!--
          The registry's payoff: one repository fixed this, so every other
          repository carrying the pattern can start from that fix.
        --%>
        <section
          :if={@fix_template}
          id="infestation-fix-template"
          class="mt-8 border-2 border-phosphor"
        >
          <div class="flex flex-wrap items-center gap-3 border-b border-rule px-4 py-3">
            <h2 class="min-w-0 flex-1 font-display text-sm uppercase tracking-[0.12em] text-phosphor">
              Fixed on
              <.link
                navigate={RepositoryPaths.repository_path(@fix_template.source_repository)}
                class="hover:underline"
              >
                {@fix_template.source_repository.owner}/{@fix_template.source_repository.name}
              </.link>
            </h2>
            <span
              :if={@propagations != []}
              class="font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint"
            >
              carried to {length(@propagations)} repo(s)
            </span>
            <button
              :if={@can_moderate and @propagation_targets > 0}
              id="infestation-propagate-fix"
              type="button"
              phx-click="propagate_fix"
              data-confirm={"Open fix jobs on #{@propagation_targets} repository(s) still carrying this pattern?"}
              class="shrink-0 bg-btn px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.12em] text-btn-fg transition hover:opacity-90"
            >
              Propagate to {@propagation_targets}
            </button>
          </div>

          <p class="whitespace-pre-line px-4 py-3 text-sm leading-6 text-ink-muted" phx-no-format>{String.slice(@fix_template.summary, 0, 600)}</p>

          <ul :if={@propagations != []} class="divide-y divide-rule border-t border-rule">
            <li
              :for={propagation <- @propagations}
              id={"propagation-#{propagation.id}"}
              class="flex flex-wrap items-baseline gap-x-3 gap-y-1 px-4 py-2.5"
            >
              <.link
                navigate={RepositoryPaths.repository_path(propagation.repository)}
                class="min-w-0 flex-1 truncate font-mono text-xs text-ink hover:text-signal hover:underline"
              >
                {propagation.repository.owner}/{propagation.repository.name}
              </.link>
              <.link
                :if={propagation.review_task}
                navigate={~p"/jobs/#{propagation.review_task.id}"}
                class="shrink-0 font-mono text-[11px] text-signal hover:underline"
              >
                Job →
              </.link>
              <span class={[
                "shrink-0 font-mono text-[10px] uppercase tracking-[0.12em]",
                propagation.status == "fixed" && "text-phosphor",
                propagation.status == "stale" && "text-ink-faint",
                propagation.status == "open" && "text-ink-muted"
              ]}>
                {propagation.status}
              </span>
            </li>
          </ul>
        </section>

        <section class="mt-8">
          <h2 class="mb-3 font-display text-lg uppercase tracking-[0.02em] text-ink">
            Affected repositories
          </h2>
          <.infestation_graph
            id="infestation-graph"
            infestation={@infestation}
            repos={repo_rows(@graph_repos)}
            pattern_key={@pattern_key}
          />
          <p
            :if={@graph_hidden > 0}
            id="infestation-graph-more"
            class="mt-3 font-mono text-[11px] text-ink-faint"
          >
            +{@graph_hidden} more repositories
          </p>
        </section>

        <section id="infestation-instances" class="mt-10">
          <h2 class="mb-3 font-display text-lg uppercase tracking-[0.02em] text-ink">
            Instances
          </h2>

          <p :if={@ledger_empty?} class="mt-4 font-mono text-xs text-ink-faint">No instances.</p>

          <ul
            id="infestation-ledger"
            phx-update="stream"
            class="mt-4 divide-y divide-rule border-2 border-strong"
          >
            <li
              :for={{dom_id, instance} <- @streams.ledger_instances}
              id={dom_id}
              class="grid gap-2 px-4 py-4 sm:grid-cols-[auto_1fr_auto] sm:items-center sm:px-6"
            >
              <span class={[
                "size-2.5 shrink-0 rounded-full",
                instance.status == "open" && "bg-phosphor",
                instance.status == "verified" && "bg-ink",
                instance.status == "fixed" && "bg-ink-muted",
                instance.status not in ["open", "verified", "fixed"] && "bg-ink-faint"
              ]}></span>
              <div class="min-w-0">
                <p class="font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
                  <.link
                    :if={instance[:host]}
                    navigate={RepositoryPaths.repository_path(instance)}
                    class="text-ink-muted hover:text-signal hover:underline"
                  >
                    {instance.owner}/{instance.name}
                  </.link>
                  <span :if={is_nil(instance[:host])}>{instance.owner}/{instance.name}</span>
                  <span class="mx-1">·</span>
                  {instance.status}
                  <span :if={instance[:commit_sha]} class="mx-1">·</span>
                  <span :if={instance[:commit_sha]} title={instance.commit_sha}>
                    {String.slice(instance.commit_sha || "", 0, 7)}
                  </span>
                </p>
                <p class="mt-1 truncate font-mono text-xs text-ink-muted">
                  {instance.file_path}
                </p>
              </div>
              <.link
                :if={instance.occurrence_public_id}
                navigate={~p"/findings/#{instance.occurrence_public_id}"}
                class="font-mono text-xs text-signal hover:underline"
              >
                Open →
              </.link>
            </li>
          </ul>

          <button
            :if={@ledger_cursor}
            id="infestation-load-more"
            type="button"
            phx-click="load_more_instances"
            class="mt-4 bg-panel px-4 py-2 font-mono text-[11px] uppercase tracking-[0.12em] text-ink transition hover:border-signal"
          >
            Load more
          </button>
        </section>
      </Layouts.page>
    </Layouts.app>
    """
  end

  # Repo matrix expects instance-shaped maps (status, owner, name, occurrence_public_id, id).
  # Rows are pattern_repos: one per repository, rolled up from the instances
  # listed below. instance_count/open_count are what make this table worth
  # showing separately - without them a repo hit twice looks like one hit once.
  defp repo_rows(repos) do
    Enum.map(repos, fn r ->
      %{
        id: r.repository_id,
        host: r.host,
        owner: r.owner,
        name: r.name,
        status: r.status,
        occurrence_public_id: r.occurrence_public_id,
        instance_count: r.instance_count,
        open_count: r.open_count
      }
    end)
  end
end
