defmodule TarakanWeb.FeedController do
  @moduledoc """
  Atom 1.0 feeds over the public record.

  Both feeds reuse the anonymous disclosure rules: findings come from
  `Scans.list_recent_disclosed_findings/1` (listed repositories, fully public
  reviews, terminal trust states), infestations from `Infestations.list_infestations/1`
  (public canonical findings on listed repositories only).
  """
  use TarakanWeb, :controller

  alias Tarakan.Infestations
  alias Tarakan.Scans

  @feed_limit 50
  @cache_control "public, max-age=300"

  def findings(conn, _params) do
    base = TarakanWeb.Endpoint.url()

    entries =
      [limit: @feed_limit]
      |> Scans.list_recent_disclosed_findings()
      |> Enum.map(fn %{canonical: canonical, repository: repository} = row ->
        url = base <> ~p"/findings/#{row.occurrence_public_id}"

        %{
          id: url,
          link: url,
          title: canonical.title,
          updated: canonical.updated_at,
          summary:
            "#{String.capitalize(canonical.severity)} in " <>
              "#{repository.owner}/#{repository.name} (#{canonical.file_path}): " <>
              truncate(canonical.description, 240)
        }
      end)

    send_feed(conn, "Tarakan findings", "#{base}/feeds/findings.xml", entries)
  end

  def infestations(conn, _params) do
    base = TarakanWeb.Endpoint.url()

    entries =
      [days: 30, limit: @feed_limit]
      |> Infestations.list_infestations()
      |> Enum.map(fn pattern ->
        url = base <> ~p"/infestations/#{pattern.pattern_key}"

        %{
          id: url,
          link: url,
          title: pattern.title,
          updated: pattern.last_seen_at || pattern.first_seen_at || DateTime.utc_now(),
          summary:
            "Seen in #{pattern.repo_count} repositories over 30 days: " <>
              "#{pattern.open_count} open, #{pattern.verified_count} verified, " <>
              "#{pattern.fixed_count} fixed, #{pattern.disputed_count} disputed."
        }
      end)

    send_feed(conn, "Tarakan infestations", "#{base}/feeds/infestations.xml", entries)
  end

  defp send_feed(conn, title, self_url, entries) do
    base = TarakanWeb.Endpoint.url()

    updated =
      entries |> Enum.map(& &1.updated) |> Enum.max(DateTime, fn -> DateTime.utc_now() end)

    body = [
      ~s(<?xml version="1.0" encoding="UTF-8"?>),
      ~s(<feed xmlns="http://www.w3.org/2005/Atom">),
      "<title>",
      xml_escape(title),
      "</title>",
      "<id>",
      xml_escape(self_url),
      "</id>",
      ~s(<link rel="self" href="),
      xml_escape(self_url),
      ~s("/>),
      ~s(<link rel="alternate" href="),
      xml_escape(base),
      ~s("/>),
      "<updated>",
      rfc3339(updated),
      "</updated>",
      Enum.map(entries, &entry_xml/1),
      "</feed>"
    ]

    conn
    |> put_resp_content_type("application/atom+xml")
    |> put_resp_header("cache-control", @cache_control)
    |> send_resp(200, body)
  end

  defp entry_xml(entry) do
    [
      "<entry>",
      "<id>",
      xml_escape(entry.id),
      "</id>",
      "<title>",
      xml_escape(entry.title),
      "</title>",
      "<updated>",
      rfc3339(entry.updated),
      "</updated>",
      ~s(<link href="),
      xml_escape(entry.link),
      ~s("/>),
      "<summary>",
      xml_escape(entry.summary),
      "</summary>",
      "</entry>"
    ]
  end

  defp rfc3339(%DateTime{} = datetime) do
    datetime |> DateTime.truncate(:second) |> DateTime.to_iso8601()
  end

  defp truncate(text, max) when byte_size(text) <= max, do: text
  defp truncate(text, max), do: String.slice(text, 0, max - 1) <> "…"

  defp xml_escape(nil), do: ""

  defp xml_escape(text) do
    text
    |> String.replace("&", "&amp;")
    |> String.replace("<", "&lt;")
    |> String.replace(">", "&gt;")
    |> String.replace("\"", "&quot;")
    |> String.replace("'", "&apos;")
  end
end
