defmodule TarakanWeb.API.RepositoryController do
  @moduledoc """
  Discovery and registration of public repositories for scanning clients.

  `GET` lists reviewable repositories (`status=unscanned` for the work queue).
  `POST` registers a public GitHub repository by URL or `owner/name` so mass
  importers (awesome lists, OSINT harvests) can feed the registry.
  """
  use TarakanWeb, :controller

  alias Tarakan.Repositories

  def index(conn, params) do
    opts =
      [
        status: params["status"],
        limit: parse_limit(params["limit"]),
        min_stars: parse_min_stars(params["min_stars"]),
        language: parse_language(params["language"] || params["lang"])
      ]
      |> Enum.reject(fn {_k, v} -> is_nil(v) or v == "" end)

    repositories = Repositories.list_reviewable_repositories(opts)

    json(conn, %{repositories: Enum.map(repositories, &repository_json/1)})
  end

  @doc """
  Registers a public GitHub repository.

  Body: `{"url": "owner/name"}` or `{"url": "https://github.com/owner/name"}`.
  Idempotent: an already-registered repository is returned as 200.
  """
  def create(conn, params) do
    url =
      params
      |> Map.get("url")
      |> Kernel.||(Map.get(params, "repository"))
      |> case do
        value when is_binary(value) -> String.trim(value)
        _ -> ""
      end

    if url == "" do
      conn
      |> put_status(:unprocessable_entity)
      |> json(%{errors: %{url: ["is required"]}})
    else
      case Repositories.register_github_repository(url, conn.assigns.current_scope) do
        {:ok, repository} ->
          # Idempotent: 200 whether newly inserted or already present.
          json(conn, %{repository: repository_json(repository)})

        {:error, reason} ->
          registration_error(conn, reason)
      end
    end
  end

  defp registration_error(conn, :invalid_github_repository) do
    conn
    |> put_status(:unprocessable_entity)
    |> json(%{error: "url must be a public GitHub owner/name or repository URL"})
  end

  defp registration_error(conn, reason) when reason in [:not_found, :not_public] do
    conn
    |> put_status(:not_found)
    |> json(%{error: "repository was not found or is not public"})
  end

  defp registration_error(conn, :registration_limit) do
    conn
    |> put_status(:too_many_requests)
    |> json(%{error: "daily repository registration limit reached"})
  end

  defp registration_error(conn, reason) when reason in [:rate_limited, :request_limited] do
    conn
    |> put_status(:too_many_requests)
    |> json(%{error: "too many registration requests; try again shortly"})
  end

  defp registration_error(conn, :unavailable) do
    conn
    |> put_status(:service_unavailable)
    |> json(%{error: "could not reach GitHub to verify the repository"})
  end

  defp registration_error(conn, :unauthorized) do
    conn
    |> put_status(:forbidden)
    |> json(%{error: "this credential cannot register repositories"})
  end

  defp registration_error(conn, _reason) do
    conn
    |> put_status(:unprocessable_entity)
    |> json(%{error: "repository could not be registered"})
  end

  defp parse_limit(nil), do: 100

  defp parse_limit(value) when is_binary(value) do
    case Integer.parse(value) do
      {limit, ""} when limit > 0 -> min(limit, 500)
      _other -> 100
    end
  end

  defp parse_limit(_), do: 100

  defp parse_min_stars(nil), do: nil

  defp parse_min_stars(value) do
    case Integer.parse(to_string(value)) do
      {n, _} when n > 0 -> n
      _ -> nil
    end
  end

  defp parse_language(nil), do: nil

  defp parse_language(value) when is_binary(value) do
    case String.trim(value) do
      "" -> nil
      lang -> lang
    end
  end

  defp parse_language(_), do: nil

  defp repository_json(repository) do
    status = Map.get(repository, :status) || repository.listing_status

    %{
      host: repository.host,
      owner: repository.owner,
      name: repository.name,
      status: status,
      listing_status: repository.listing_status,
      default_branch: repository.default_branch,
      primary_language: repository.primary_language,
      stars_count: repository.stars_count,
      scan_count: repository.scan_count,
      last_scanned_at: repository.last_scanned_at,
      registered_at: repository.inserted_at,
      record_url:
        TarakanWeb.Endpoint.url() <> TarakanWeb.RepositoryPaths.repository_path(repository)
    }
  end
end
