defmodule TarakanWeb.API.InfestationController do
  @moduledoc """
  Public read access to the cross-repository infestation map.

  Patterns aggregate only publicly disclosed canonical findings on listed
  repositories, so listing and detail responses need no further filtering.
  Query params are whitelisted: `days` buckets to 7/30/90/365, `min_repos`
  clamps to 2..50, `limit` to 100.
  """
  use TarakanWeb, :controller

  alias Tarakan.FindingSignals
  alias Tarakan.Infestations

  @days [7, 30, 90, 365]
  @default_days 30
  @default_limit 40
  @max_limit 100

  def index(conn, params) do
    patterns =
      Infestations.list_infestations(
        days: parse_days(params["days"]),
        min_repos: parse_min_repos(params["min_repos"]),
        limit: parse_limit(params["limit"])
      )

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

  def show(conn, %{"pattern_key" => pattern_key}) do
    case Infestations.get_infestation(pattern_key) do
      nil ->
        conn
        |> put_status(:not_found)
        |> json(%{error: "pattern was not found"})

      infestation ->
        page = Infestations.list_instances_page(pattern_key, limit: 50)

        json(conn, %{
          pattern: pattern_json(infestation),
          instances: Enum.map(page.entries, &instance_json/1)
        })
    end
  end

  @doc """
  Vaccine pack: the validated detector for this infestation's dominant code
  cluster, as a rule file.

  Free, and 404s when no worker has produced a rule that actually matches known
  instances yet. The previous version of this endpoint always returned something,
  and what it returned matched nothing.
  """
  def vaccine(conn, %{"pattern_key" => pattern_key}) do
    with %{} = infestation <- Infestations.get_infestation(pattern_key),
         cluster when is_binary(cluster) <- FindingSignals.dominant_cluster(pattern_key),
         rule when not is_nil(rule) <- FindingSignals.best_rule(cluster) do
      conn
      |> put_resp_content_type("application/x-yaml")
      |> send_resp(200, vaccine_yaml(infestation, rule))
    else
      nil ->
        conn
        |> put_status(:not_found)
        |> json(%{
          error: "no validated detector for this pattern yet",
          pattern_key: pattern_key
        })
    end
  end

  # The rule is served verbatim as the worker wrote and validated it. The header
  # carries the provenance so a reader can tell how much the detector was tested
  # rather than having to trust it.
  defp vaccine_yaml(infestation, rule) do
    """
    # Tarakan vaccine pack for #{infestation.pattern_key}
    # #{infestation.title}
    #
    # Engine:    #{rule.engine}
    # Validated: matched #{rule.matched_count} of #{rule.checked_count} known instances
    #            at #{rule.validated_at}
    # Source:    #{TarakanWeb.Endpoint.url()}/infestations/#{infestation.pattern_key}
    #
    # This detector was written and validated against real instances by a
    # contributor's agent. Review what it matches before gating CI on it.
    #{rule.rule_yaml}
    """
  end

  @doc """
  Submits a detector for one code cluster.

  The worker validates before submitting: it has both the rule engine and the
  code, so it runs the rule against known members and reports which ones it hit.
  `matched_finding_ids` is the receipt for `matched_count` and must list exactly
  the findings that matched, or the submission is rejected.
  """
  def put_rule(conn, %{"code_pattern_key" => key} = params) do
    scope = conn.assigns[:current_scope]

    cond do
      is_nil(scope) || is_nil(scope.account) ->
        conn
        |> put_status(:unauthorized)
        |> json(%{error: "missing or invalid API token"})

      true ->
        attrs = %{
          engine: params["engine"] || "semgrep",
          language: params["language"],
          rule_yaml: params["rule_yaml"],
          checked_count: params["checked_count"] || 0,
          matched_count: params["matched_count"] || 0,
          matched_finding_ids: params["matched_finding_ids"] || []
        }

        case FindingSignals.put_rule(scope, key, attrs) do
          {:ok, rule} ->
            conn
            |> put_status(:created)
            |> json(%{
              code_pattern_key: rule.code_pattern_key,
              engine: rule.engine,
              matched_count: rule.matched_count,
              checked_count: rule.checked_count,
              validated_at: rule.validated_at,
              # An unvalidated rule is stored but never served, so say so rather
              # than letting the worker assume it shipped.
              servable: Tarakan.Scans.CodePatternRule.servable?(rule)
            })

          {:error, :unauthorized} ->
            conn |> put_status(:forbidden) |> json(%{error: "not authorized"})

          {:error, %Ecto.Changeset{} = changeset} ->
            conn
            |> put_status(:unprocessable_entity)
            |> json(%{errors: TarakanWeb.ChangesetErrors.to_map(changeset)})
        end
    end
  end

  defp parse_days(value) do
    case Integer.parse(to_string(value)) do
      {days, ""} when days in @days -> days
      _other -> @default_days
    end
  end

  defp parse_min_repos(nil), do: 2

  defp parse_min_repos(value) do
    case Integer.parse(to_string(value)) do
      {min_repos, ""} -> min_repos |> max(2) |> min(50)
      _other -> 2
    end
  end

  defp parse_limit(nil), do: @default_limit

  defp parse_limit(value) do
    case Integer.parse(to_string(value)) do
      {limit, ""} when limit > 0 -> min(limit, @max_limit)
      _other -> @default_limit
    end
  end

  defp pattern_json(pattern) do
    %{
      pattern_key: pattern.pattern_key,
      title: pattern.title,
      severity: pattern.severity,
      repo_count: pattern.repo_count,
      instance_count: pattern.instance_count,
      open_count: pattern.open_count,
      verified_count: pattern.verified_count,
      fixed_count: pattern.fixed_count,
      disputed_count: pattern.disputed_count,
      first_seen_at: pattern.first_seen_at,
      last_seen_at: pattern.last_seen_at,
      sample_file_path: pattern.sample_file_path,
      sample_occurrence_public_id: pattern.sample_occurrence_public_id,
      record_url: TarakanWeb.Endpoint.url() <> ~p"/infestations/#{pattern.pattern_key}"
    }
  end

  defp instance_json(instance) do
    %{
      public_id: instance.public_id,
      occurrence_public_id: instance.occurrence_public_id,
      status: instance.status,
      severity: instance.severity,
      title: instance.title,
      file_path: instance.file_path,
      detections_count: instance.detections_count,
      confirmations_count: instance.confirmations_count,
      repository: %{
        host: instance.host,
        owner: instance.owner,
        name: instance.name
      },
      updated_at: instance.updated_at
    }
  end
end
