defmodule TarakanWeb.RepositoryCodeLive do
  use TarakanWeb, :live_view

  alias Tarakan.Repositories
  alias Tarakan.Repositories.Repository
  alias Tarakan.RepositoryCode
  alias Tarakan.RepositoryCode.{File, Tree}
  alias Tarakan.RepositoryPath
  alias Tarakan.Scans
  alias TarakanWeb.RepositoryPaths

  @commit_sha_pattern ~r/^[0-9a-f]{40}$/
  @line_range_pattern ~r/^([1-9][0-9]{0,8})(?:-([1-9][0-9]{0,8}))?$/
  @max_rendered_lines 10_000
  @max_selected_line 1_000_000
  @max_selected_span 1_000

  @impl true
  def mount(params, _session, socket) do
    {action, params} = normalize_route_params(socket.assigns.live_action, params)
    socket = assign(socket, :live_action, action)

    case source_from_params(action, params, socket) do
      {:ok, source} ->
        repository = source.repository

        if connected?(socket) do
          Repositories.subscribe()
          Scans.subscribe(repository.id)
        end

        {:ok,
         socket
         |> assign(:page_title, "Code · #{repository.owner}/#{repository.name}")
         |> assign(:repository, repository)
         |> assign(:clone_urls, RepositoryPaths.clone_urls(repository))
         |> assign(:finding, source.finding)
         |> assign(:finding_ref, source.finding && source.finding.public_id)
         |> assign(:source_commit_sha, source.commit_sha)
         |> assign(:source_path, source.path)
         |> assign(:source_line_range, source.line_range)
         |> assign(:commit_sha, nil)
         |> assign(:path, "")
         |> assign(:path_segments, [])
         |> assign(:line_range, nil)
         |> assign(:line_range_invalid?, false)
         |> assign(:view_state, :loading)
         |> assign(:browser_kind, nil)
         |> assign(:tree, nil)
         |> assign(:file, nil)
         |> assign(:entry_count, 0)
         |> assign(:visible_finding_count, 0)
         |> assign(:line_count, 0)
         |> assign(:line_range_outside_file?, false)
         |> assign(:suspicious_controls?, false)
         |> assign(:error_kind, nil)
         |> assign(:request_id, nil)
         |> assign(:branch_options, [])
         |> assign(:selected_branch, repository.default_branch)
         |> maybe_load_branch_options(action)
         |> stream_configure(:entries, dom_id: &entry_dom_id/1)
         |> stream_configure(:lines, dom_id: &line_dom_id/1)
         |> stream(:entries, [])
         |> stream(:lines, [])}

      {:error, :not_found} ->
        raise Ecto.NoResultsError, queryable: Repository
    end
  end

  @impl true
  def handle_params(params, _uri, socket) do
    {action, params} = normalize_route_params(socket.assigns.live_action, params)

    socket =
      case action do
        :entry -> prepare_entry_request(socket)
        :finding -> prepare_finding_request(socket)
        :show -> prepare_code_request(socket, params)
      end

    {:noreply, maybe_start_request(assign(socket, :live_action, action))}
  end

  @impl true
  def handle_event("retry", _params, socket) do
    {:noreply,
     socket
     |> assign(:view_state, :loading)
     |> assign(:error_kind, nil)
     |> maybe_start_request()}
  end

  # Switch the code browser to another branch tip (still commit-pinned under the hood).
  def handle_event("select_branch", %{"branch" => branch}, socket) do
    if socket.assigns.live_action == :finding do
      {:noreply, put_flash(socket, :error, "Finding context is pinned to its report commit.")}
    else
      repository = socket.assigns.repository
      branch = String.trim(to_string(branch || ""))

      case RepositoryCode.resolve_branch_commit(repository, branch) do
        {:ok, commit_sha} ->
          path =
            if is_binary(socket.assigns.path) and socket.assigns.path != "" do
              split_path(socket.assigns.path)
            else
              []
            end

          {:noreply,
           socket
           |> assign(:selected_branch, branch)
           |> push_navigate(to: code_path(repository, commit_sha, path))}

        {:error, _reason} ->
          {:noreply,
           put_flash(socket, :error, "Could not resolve branch #{inspect(branch)} to a commit.")}
      end
    end
  end

  @impl true
  def handle_async(:browse, {:ok, {request_id, result}}, socket) do
    if request_id == socket.assigns.request_id do
      case reauthorize_view(socket) do
        {:ok, authorized_socket} ->
          {:noreply, apply_browse_result(authorized_socket, result)}

        {:error, :not_found} ->
          {:noreply, evict_source(socket)}
      end
    else
      {:noreply, socket}
    end
  end

  def handle_async(:browse, {:exit, _reason}, socket) do
    {:noreply, assign_error(socket, :unavailable)}
  end

  @impl true
  def handle_info(
        {:repository_record_updated, %Repository{id: repository_id}},
        %{assigns: %{repository: %Repository{id: repository_id}}} = socket
      ) do
    case reauthorize_view(socket) do
      {:ok, authorized_socket} -> {:noreply, refresh_finding_badges(authorized_socket)}
      {:error, :not_found} -> {:noreply, evict_source(socket)}
    end
  end

  def handle_info({event, %Repository{}}, socket)
      when event in [:repository_registered, :repository_record_updated] do
    {:noreply, socket}
  end

  def handle_info(
        {:scan_updated, %{id: scan_id}},
        %{assigns: %{live_action: :finding, finding: %{scan_id: scan_id}}} = socket
      ) do
    case reauthorize_view(socket) do
      {:ok, authorized_socket} -> {:noreply, authorized_socket}
      {:error, :not_found} -> {:noreply, evict_source(socket)}
    end
  end

  def handle_info({event, _scan}, socket) when event in [:scan_submitted, :scan_updated],
    do: {:noreply, refresh_finding_badges(socket)}

  defp source_from_params(:finding, %{"finding_ref" => finding_ref}, socket) do
    with {:ok, {scan, finding}} <- Scans.get_finding(socket.assigns.current_scope, finding_ref),
         %Repository{} = repository <-
           Repositories.get_visible_repository(
             scan.repository.host,
             scan.repository.owner,
             scan.repository.name,
             socket.assigns.current_scope
           ) do
      {:ok,
       %{
         repository: repository,
         finding: finding,
         commit_sha: scan.commit_sha,
         path: finding.file_path,
         line_range: finding_line_range(finding)
       }}
    end
  end

  defp source_from_params(_action, %{"host" => slug, "owner" => owner, "name" => name}, socket) do
    with {:ok, host} <- Tarakan.Hosts.host_for_slug(slug),
         %Repository{} = repository <-
           Repositories.get_visible_repository(host, owner, name, socket.assigns.current_scope) do
      {:ok,
       %{
         repository: repository,
         finding: nil,
         commit_sha: nil,
         path: nil,
         line_range: nil
       }}
    else
      _not_visible -> {:error, :not_found}
    end
  end

  defp source_from_params(_action, _params, _socket), do: {:error, :not_found}

  # Resolves the bare GitHub-style routes (/owner/name...), which carry no
  # :host segment. A handle-like first segment is a Tarakan-hosted
  # repository. A host-like first segment means the bare pattern's literal
  # ("code") swallowed a remote path - a repository literally named "code" -
  # so shift the params: /github.com/rails/code[/code[/sha[/*path]]].
  # The security tab of such a repository has no reachable URL form; those
  # requests fall through to :not_found.
  defp normalize_route_params(action, %{"owner" => owner, "name" => name} = params)
       when not is_map_key(params, "host") do
    if Tarakan.Hosts.host_segment?(owner) do
      remote = %{"host" => owner, "owner" => name, "name" => "code"}

      case {action, params["commit_sha"], params["path"]} do
        {:code_entry, _sha, _path} ->
          {:entry, remote}

        {:show, "code", nil} ->
          {:entry, remote}

        {:show, "code", [commit_sha | path]} ->
          {:show, remote |> Map.put("commit_sha", commit_sha) |> Map.put("path", path)}

        _other ->
          {action, params}
      end
    else
      {action_without_code_alias(action), Map.put(params, "host", Repository.hosted_host())}
    end
  end

  defp normalize_route_params(action, params), do: {action_without_code_alias(action), params}

  # /:owner/:name/code is a distinct live action only so bare-route
  # reinterpretation can tell it apart from /:owner/:name; everywhere else
  # it behaves exactly like :entry.
  defp action_without_code_alias(:code_entry), do: :entry
  defp action_without_code_alias(action), do: action

  defp prepare_entry_request(socket) do
    prepare_request(socket, :resolve_entry, nil, "", nil, false)
  end

  defp prepare_finding_request(socket) do
    prepare_request(
      socket,
      :browse_finding,
      socket.assigns.source_commit_sha,
      socket.assigns.source_path,
      socket.assigns.source_line_range,
      false
    )
  end

  defp prepare_code_request(socket, params) do
    with {:ok, commit_sha} <- normalize_commit_sha(params["commit_sha"]),
         {:ok, path} <- normalize_route_path(params["path"]),
         {:ok, line_range, line_range_invalid?} <- parse_line_range(params["lines"]) do
      socket
      |> prepare_request(
        :browse,
        commit_sha,
        path,
        line_range,
        line_range_invalid?
      )
      |> sync_selected_branch(commit_sha)
    else
      {:error, reason} -> assign_error(socket, reason)
    end
  end

  # After a branch switch (or deep link to a tip SHA), keep the picker in sync.
  defp sync_selected_branch(socket, commit_sha) do
    case RepositoryCode.branch_for_commit(socket.assigns.repository, commit_sha) do
      {:ok, branch} -> assign(socket, :selected_branch, branch)
      _unknown -> socket
    end
  end

  defp prepare_request(
         socket,
         request_kind,
         commit_sha,
         path,
         line_range,
         line_range_invalid?
       ) do
    request_id = System.unique_integer([:positive, :monotonic])

    socket
    |> assign(:request_id, request_id)
    |> assign(:request_kind, request_kind)
    |> assign(:commit_sha, commit_sha)
    |> assign(:path, path)
    |> assign(:path_segments, split_path(path))
    |> assign(:line_range, line_range)
    |> assign(:line_range_invalid?, line_range_invalid?)
    |> assign(:view_state, :loading)
    |> assign(:browser_kind, nil)
    |> assign(:tree, nil)
    |> assign(:file, nil)
    |> assign(:entry_count, 0)
    |> assign(:visible_finding_count, 0)
    |> assign(:line_count, 0)
    |> assign(:line_range_outside_file?, false)
    |> assign(:suspicious_controls?, false)
    |> assign(:error_kind, nil)
    |> stream(:entries, [], reset: true)
    |> stream(:lines, [], reset: true)
  end

  defp maybe_start_request(%{assigns: %{view_state: :loading}} = socket) do
    if connected?(socket) do
      request_id = socket.assigns.request_id
      request_kind = socket.assigns.request_kind
      repository = socket.assigns.repository
      commit_sha = socket.assigns.commit_sha
      path = socket.assigns.path
      # Per-IP browsing budgets are enforced at the HTTP layer by
      # TarakanWeb.Plugs.CodeBrowserRateLimit; the reads below stay on the
      # default (non-bypassed) code path.
      socket
      |> cancel_async(:browse)
      |> start_async(:browse, fn ->
        {request_id, run_request(request_kind, repository, commit_sha, path)}
      end)
    else
      socket
    end
  end

  defp maybe_start_request(socket), do: socket

  defp run_request(:browse, repository, commit_sha, path) do
    # Generic browser may open any *current* branch tip (not only default),
    # but still rejects guessed historical SHAs that are not branch tips.
    with :ok <- RepositoryCode.authorize_public_commit(repository, commit_sha),
         {:ok, result} <- RepositoryCode.browse(repository, commit_sha, path, []) do
      {:ok, result}
    else
      # Never show the old "busy network" page for GitHub REST quota.
      {:error, :rate_limited} -> {:error, :unavailable}
      {:error, reason} -> {:error, reason}
    end
  end

  defp run_request(:browse_finding, repository, commit_sha, path) do
    case RepositoryCode.browse(repository, commit_sha, path, []) do
      {:error, :rate_limited} -> {:error, :unavailable}
      other -> other
    end
  end

  defp run_request(:resolve_entry, repository, _commit_sha, _path) do
    with {:ok, commit_sha} <- RepositoryCode.resolve_default_commit(repository, []),
         {:ok, result} <- RepositoryCode.browse(repository, commit_sha, "", []) do
      {:ok, result}
    else
      {:error, :rate_limited} -> {:error, :unavailable}
      {:error, reason} -> {:error, reason}
    end
  end

  defp apply_browse_result(socket, {:ok, %Tree{truncated: true}}) do
    assign_error(socket, :tree_truncated)
  end

  defp apply_browse_result(socket, {:ok, %Tree{} = tree}) do
    finding_paths = visible_finding_paths(socket, tree.commit_sha)

    entries =
      tree.entries
      |> sort_entries()
      |> Enum.map(&entry_with_finding_count(&1, finding_paths))

    socket
    |> assign(:view_state, :ready)
    |> assign(:browser_kind, :tree)
    |> assign(:commit_sha, tree.commit_sha)
    |> assign(:path, tree.path)
    |> assign(:path_segments, split_path(tree.path))
    |> assign(:tree, tree)
    |> assign(:file, nil)
    |> assign(:entry_count, length(entries))
    |> assign(:visible_finding_count, length(finding_paths))
    |> assign(:line_count, 0)
    |> assign(:line_range_outside_file?, false)
    |> assign(:suspicious_controls?, false)
    |> assign(:error_kind, nil)
    |> stream(:entries, entries, reset: true)
    |> stream(:lines, [], reset: true)
  end

  defp apply_browse_result(socket, {:ok, %File{} = file}) do
    case file_lines(file.content, socket.assigns.line_range) do
      {:ok, lines} ->
        line_count = length(lines)
        finding_count = visible_file_finding_count(socket, file.commit_sha, file.path)

        socket
        |> assign(:view_state, :ready)
        |> assign(:browser_kind, :file)
        |> assign(:commit_sha, file.commit_sha)
        |> assign(:path, file.path)
        |> assign(:path_segments, split_path(file.path))
        |> assign(:tree, nil)
        |> assign(:file, file)
        |> assign(:entry_count, 0)
        |> assign(:visible_finding_count, finding_count)
        |> assign(:line_count, line_count)
        |> assign(
          :line_range_outside_file?,
          line_range_outside_file?(socket.assigns.line_range, line_count)
        )
        |> assign(:suspicious_controls?, suspicious_source_controls?(file.content))
        |> assign(:error_kind, nil)
        |> stream(:entries, [], reset: true)
        |> stream(:lines, lines, reset: true)

      {:error, :too_many_lines} ->
        assign_error(socket, :blob_too_large)
    end
  end

  defp apply_browse_result(socket, {:ok, commit_sha}) when is_binary(commit_sha) do
    case normalize_commit_sha(commit_sha) do
      {:ok, commit_sha} ->
        push_navigate(socket,
          to: code_path(socket.assigns.repository, commit_sha, [])
        )

      {:error, _reason} ->
        assign_error(socket, :invalid_response)
    end
  end

  defp apply_browse_result(socket, {:error, reason}), do: assign_error(socket, reason)
  defp apply_browse_result(socket, _unexpected), do: assign_error(socket, :invalid_response)

  defp assign_error(socket, reason) do
    socket
    |> assign(:view_state, :error)
    |> assign(:browser_kind, nil)
    |> assign(:tree, nil)
    |> assign(:file, nil)
    |> assign(:entry_count, 0)
    |> assign(:visible_finding_count, 0)
    |> assign(:line_count, 0)
    |> assign(:line_range_outside_file?, false)
    |> assign(:suspicious_controls?, false)
    |> assign(:error_kind, normalize_error(reason))
    |> stream(:entries, [], reset: true)
    |> stream(:lines, [], reset: true)
  end

  defp normalize_error(reason)
       when reason in [
              :invalid_commit_sha,
              :invalid_path,
              :empty_repository,
              :identity_changed,
              :commit_mismatch,
              :not_found,
              :tree_truncated,
              :tree_too_large,
              :blob_too_large,
              :binary_blob,
              :unsupported_entry,
              :rate_limited,
              :unavailable,
              :invalid_response
            ],
       do: reason

  defp normalize_error(_reason), do: :unavailable

  defp normalize_commit_sha(commit_sha) when is_binary(commit_sha) do
    normalized = String.downcase(commit_sha)

    if Regex.match?(@commit_sha_pattern, normalized) do
      {:ok, normalized}
    else
      {:error, :invalid_commit_sha}
    end
  end

  defp normalize_commit_sha(_commit_sha), do: {:error, :invalid_commit_sha}

  defp normalize_route_path(nil), do: {:ok, ""}

  defp normalize_route_path(path) when is_list(path) do
    path
    |> Enum.join("/")
    |> RepositoryPath.normalize()
  end

  defp normalize_route_path(_path), do: {:error, :invalid_path}

  defp parse_line_range(nil), do: {:ok, nil, false}
  defp parse_line_range(""), do: {:ok, nil, false}

  defp parse_line_range(value) when is_binary(value) do
    case Regex.run(@line_range_pattern, value, capture: :all_but_first) do
      [first, ""] -> validate_single_line(first)
      [first] -> validate_single_line(first)
      [first, last] -> validate_line_range(String.to_integer(first), String.to_integer(last))
      nil -> {:ok, nil, true}
    end
  end

  defp parse_line_range(_value), do: {:ok, nil, true}

  defp validate_single_line(line) do
    line = String.to_integer(line)
    validate_line_range(line, line)
  end

  defp validate_line_range(first, last)
       when last >= first and last <= @max_selected_line and
              last - first <= @max_selected_span,
       do: {:ok, {first, last}, false}

  defp validate_line_range(_first, _last), do: {:ok, nil, true}

  defp finding_line_range(%{line_start: line_start, line_end: line_end})
       when is_integer(line_start) do
    {line_start, line_end || line_start}
  end

  defp finding_line_range(_finding), do: nil

  defp split_path(""), do: []
  defp split_path(path), do: String.split(path, "/", trim: true)

  defp file_lines("", _line_range), do: {:ok, []}

  defp file_lines(content, line_range) do
    lines = String.split(content, "\n", parts: @max_rendered_lines + 1, trim: false)

    if length(lines) > @max_rendered_lines do
      {:error, :too_many_lines}
    else
      {:ok,
       lines
       |> Enum.with_index(1)
       |> Enum.map(fn {content, number} ->
         %{number: number, content: content, highlighted?: line_selected?(number, line_range)}
       end)}
    end
  end

  defp line_selected?(_number, nil), do: false
  defp line_selected?(number, {first, last}), do: number >= first and number <= last

  defp line_range_outside_file?(nil, _line_count), do: false

  defp line_range_outside_file?({first, last}, line_count),
    do: first > line_count or last > line_count

  defp sort_entries(entries) do
    Enum.sort_by(entries, fn entry ->
      {entry_type_order(entry.type), String.downcase(entry.name), entry.name}
    end)
  end

  defp entry_with_finding_count(entry, finding_paths) do
    count =
      case entry.type do
        :tree ->
          prefix = entry.path <> "/"
          Enum.count(finding_paths, &String.starts_with?(&1, prefix))

        :blob ->
          Enum.count(finding_paths, &(&1 == entry.path))

        _unsupported ->
          0
      end

    entry
    |> Map.from_struct()
    |> Map.put(:finding_count, count)
  end

  defp visible_file_finding_count(socket, commit_sha, path) do
    socket
    |> visible_finding_paths(commit_sha)
    |> Enum.count(&(&1 == path))
  end

  defp visible_finding_paths(socket, commit_sha) do
    socket.assigns.current_scope
    |> Scans.list_scans(socket.assigns.repository)
    |> Enum.filter(
      &(&1.commit_sha == commit_sha and &1.details_visible and &1.review_status != "rejected")
    )
    |> Enum.flat_map(& &1.findings)
    |> Enum.map(& &1.file_path)
  end

  defp refresh_finding_badges(%{assigns: %{browser_kind: :tree, tree: %Tree{} = tree}} = socket),
    do: apply_browse_result(socket, {:ok, tree})

  defp refresh_finding_badges(%{assigns: %{browser_kind: :file, file: %File{} = file}} = socket) do
    assign(
      socket,
      :visible_finding_count,
      visible_file_finding_count(socket, file.commit_sha, file.path)
    )
  end

  defp refresh_finding_badges(socket), do: socket

  defp entry_type_order(:tree), do: 0
  defp entry_type_order(:blob), do: 1
  defp entry_type_order(:symlink), do: 2
  defp entry_type_order(:submodule), do: 3

  defp entry_dom_id(entry) do
    encoded_path = Base.url_encode64(entry.path, padding: false)
    "code-entry-#{encoded_path}"
  end

  defp line_dom_id(line), do: "L#{line.number}"

  defp repository_record_path(repository), do: RepositoryPaths.repository_path(repository)

  defp repository_security_path(repository),
    do: RepositoryPaths.repository_security_path(repository)

  defp code_path(repository, commit_sha, path_segments),
    do: RepositoryPaths.repository_code_path(repository, commit_sha, path_segments)

  # Finding views stay on their report pin; free browse can switch branches.
  defp maybe_load_branch_options(socket, :finding), do: socket

  defp maybe_load_branch_options(socket, _action) do
    repository = socket.assigns.repository

    case RepositoryCode.list_branches(repository) do
      {:ok, branches} ->
        socket
        |> assign(:branch_options, branches)
        |> assign(
          :selected_branch,
          socket.assigns.selected_branch || repository.default_branch || List.first(branches)
        )

      {:error, _} ->
        default = repository.default_branch
        branches = if is_binary(default) and default != "", do: [default], else: []

        socket
        |> assign(:branch_options, branches)
        |> assign(:selected_branch, default)
    end
  end

  defp breadcrumb_items(path_segments) do
    path_segments
    |> Enum.with_index()
    |> Enum.map(fn {name, index} ->
      %{name: name, segments: Enum.take(path_segments, index + 1)}
    end)
  end

  defp entry_path(repository, commit_sha, entry) do
    code_path(repository, commit_sha, split_path(entry.path))
  end

  defp breadcrumb_path(repository, commit_sha, segments) do
    code_path(repository, commit_sha, segments)
  end

  defp line_path(live_action, finding_ref, repository, commit_sha, path_segments, line_number) do
    base =
      if live_action == :finding do
        ~p"/findings/#{finding_ref}/code"
      else
        code_path(repository, commit_sha, path_segments)
      end

    if live_action == :finding do
      base <> "#L#{line_number}"
    else
      base <> "?lines=#{line_number}#L#{line_number}"
    end
  end

  defp format_bytes(nil), do: "-"
  defp format_bytes(bytes) when bytes < 1_024, do: "#{bytes} B"

  defp format_bytes(bytes) when bytes < 1_024 * 1_024,
    do: "#{Float.round(bytes / 1_024, 1)} KB"

  defp format_bytes(bytes), do: "#{Float.round(bytes / (1_024 * 1_024), 1)} MB"

  defp short_sha(sha), do: String.slice(sha, 0, 7)

  defp entry_icon(:tree), do: "hero-folder"
  defp entry_icon(:blob), do: "hero-document-text"
  defp entry_icon(:symlink), do: "hero-link"
  defp entry_icon(:submodule), do: "hero-cube"

  defp entry_label(:symlink), do: "symbolic link"
  defp entry_label(:submodule), do: "submodule"

  defp error_title(:invalid_commit_sha), do: "Invalid commit"
  defp error_title(:invalid_path), do: "Invalid path"
  defp error_title(:not_found), do: "Source not found"
  defp error_title(:binary_blob), do: "Binary file"
  defp error_title(:blob_too_large), do: "File too large"
  defp error_title(:tree_too_large), do: "Directory too large"
  defp error_title(:tree_truncated), do: "Incomplete directory"
  defp error_title(:unsupported_entry), do: "Unsupported source entry"
  defp error_title(:empty_repository), do: "Empty repository"
  defp error_title(:rate_limited), do: "Code unavailable"
  defp error_title(:identity_changed), do: "Repository identity changed"
  defp error_title(:commit_mismatch), do: "Commit verification failed"
  defp error_title(_error), do: "Code unavailable"

  defp error_message(:invalid_commit_sha),
    do: "Code views require a full 40-character commit SHA."

  defp error_message(:invalid_path), do: "That repository path is not valid."

  defp error_message(:not_found),
    do: "No source exists at this path in the pinned commit."

  defp error_message(:binary_blob),
    do: "Tarakan does not render binary files as source code."

  defp error_message(:blob_too_large),
    do: "This file exceeds the safe source-preview limit."

  defp error_message(reason) when reason in [:tree_too_large, :tree_truncated],
    do: "This directory cannot be represented safely as a complete listing."

  defp error_message(:unsupported_entry),
    do: "This Git object type cannot be opened in the source browser."

  defp error_message(:empty_repository),
    do: "Nothing has been pushed to this repository yet. Push a branch and it will appear here."

  defp error_message(:rate_limited),
    do:
      "Could not load this commit from git or GitHub yet. Wait a few seconds for the mirror to fill, then retry."

  defp error_message(:unavailable),
    do:
      "Could not load this commit from git or GitHub yet. Wait a few seconds for the mirror to fill, then retry."

  defp error_message(reason) when reason in [:identity_changed, :commit_mismatch],
    do: "Tarakan could not verify this source against the registered public repository."

  defp error_message(_error),
    do:
      "Could not load this commit from git or GitHub yet. Wait a few seconds for the mirror to fill, then retry."

  defp suspicious_source_controls?(content) do
    String.contains?(content, <<0>>) or
      Enum.any?([0x202A, 0x202B, 0x202C, 0x202D, 0x202E, 0x2066, 0x2067, 0x2068, 0x2069], fn
        codepoint -> String.contains?(content, <<codepoint::utf8>>)
      end)
  end

  defp reauthorize_view(%{assigns: %{live_action: :finding}} = socket) do
    repository = socket.assigns.repository

    with %Repository{} = visible_repository <-
           Repositories.get_visible_repository(
             repository.host,
             repository.owner,
             repository.name,
             socket.assigns.current_scope
           ),
         {:ok, {scan, finding}} <-
           Scans.get_finding(socket.assigns.current_scope, socket.assigns.finding_ref),
         true <- scan.repository_id == visible_repository.id,
         true <- scan.commit_sha == socket.assigns.source_commit_sha,
         true <- finding.file_path == socket.assigns.source_path do
      {:ok,
       socket
       |> assign(:repository, visible_repository)
       |> assign(:finding, finding)}
    else
      _not_visible -> {:error, :not_found}
    end
  end

  defp reauthorize_view(socket) do
    repository = socket.assigns.repository

    case Repositories.get_visible_repository(
           repository.host,
           repository.owner,
           repository.name,
           socket.assigns.current_scope
         ) do
      %Repository{} = visible_repository ->
        {:ok, assign(socket, :repository, visible_repository)}

      nil ->
        {:error, :not_found}
    end
  end

  defp evict_source(socket) do
    repository = socket.assigns.repository

    destination =
      case Repositories.get_visible_repository(
             repository.host,
             repository.owner,
             repository.name,
             socket.assigns.current_scope
           ) do
        %Repository{} = visible_repository -> repository_record_path(visible_repository)
        nil -> ~p"/"
      end

    socket
    |> clear_source()
    |> push_navigate(to: destination)
  end

  defp clear_source(socket) do
    socket
    |> assign(:view_state, :loading)
    |> assign(:browser_kind, nil)
    |> assign(:commit_sha, nil)
    |> assign(:path, "")
    |> assign(:path_segments, [])
    |> assign(:line_range, nil)
    |> assign(:finding, nil)
    |> assign(:tree, nil)
    |> assign(:file, nil)
    |> assign(:entry_count, 0)
    |> assign(:visible_finding_count, 0)
    |> assign(:line_count, 0)
    |> assign(:line_range_outside_file?, false)
    |> assign(:suspicious_controls?, false)
    |> assign(:error_kind, nil)
    |> stream(:entries, [], reset: true)
    |> stream(:lines, [], reset: true)
  end
end
